Upgrade Boehm GC to 7.2alpha4.
[cacao.git] / src / mm / boehm-gc / pthread_stop_world.c
1 /*
2  * Copyright (c) 1994 by Xerox Corporation.  All rights reserved.
3  * Copyright (c) 1996 by Silicon Graphics.  All rights reserved.
4  * Copyright (c) 1998 by Fergus Henderson.  All rights reserved.
5  * Copyright (c) 2000-2009 by Hewlett-Packard Development Company.
6  * All rights reserved.
7  *
8  * THIS MATERIAL IS PROVIDED AS IS, WITH ABSOLUTELY NO WARRANTY EXPRESSED
9  * OR IMPLIED.  ANY USE IS AT YOUR OWN RISK.
10  *
11  * Permission is hereby granted to use or copy this program
12  * for any purpose,  provided the above notices are retained on all copies.
13  * Permission to modify the code and to distribute modified code is granted,
14  * provided the above notices are retained, and a notice that the code was
15  * modified is included with the above copyright notice.
16  */
17
18 #include "private/pthread_support.h"
19
20 #if defined(GC_PTHREADS) && !defined(GC_WIN32_THREADS) && \
21     !defined(GC_DARWIN_THREADS)
22
23 #ifndef GC_OPENBSD_THREADS
24
25 #include <signal.h>
26 #include <semaphore.h>
27 #include <errno.h>
28 #include <unistd.h>
29 #include "atomic_ops.h"
30
31 #ifdef DEBUG_THREADS
32
33 #ifndef NSIG
34 # if defined(MAXSIG)
35 #  define NSIG (MAXSIG+1)
36 # elif defined(_NSIG)
37 #  define NSIG _NSIG
38 # elif defined(__SIGRTMAX)
39 #  define NSIG (__SIGRTMAX+1)
40 # else
41   --> please fix it
42 # endif
43 #endif
44
45 /* It's safe to call original pthread_sigmask() here. */
46 #undef pthread_sigmask
47
48 void GC_print_sig_mask(void)
49 {
50     sigset_t blocked;
51     int i;
52
53     if (pthread_sigmask(SIG_BLOCK, NULL, &blocked) != 0)
54         ABORT("pthread_sigmask");
55     GC_printf("Blocked: ");
56     for (i = 1; i < NSIG; i++) {
57         if (sigismember(&blocked, i)) { GC_printf("%d ", i); }
58     }
59     GC_printf("\n");
60 }
61
62 #endif /* DEBUG_THREADS */
63
64 /* Remove the signals that we want to allow in thread stopping  */
65 /* handler from a set.                                          */
66 STATIC void GC_remove_allowed_signals(sigset_t *set)
67 {
68     if (sigdelset(set, SIGINT) != 0
69           || sigdelset(set, SIGQUIT) != 0
70           || sigdelset(set, SIGABRT) != 0
71           || sigdelset(set, SIGTERM) != 0) {
72         ABORT("sigdelset() failed");
73     }
74
75 #   ifdef MPROTECT_VDB
76       /* Handlers write to the thread structure, which is in the heap,  */
77       /* and hence can trigger a protection fault.                      */
78       if (sigdelset(set, SIGSEGV) != 0
79 #         ifdef SIGBUS
80             || sigdelset(set, SIGBUS) != 0
81 #         endif
82           ) {
83         ABORT("sigdelset() failed");
84       }
85 #   endif
86 }
87
88 static sigset_t suspend_handler_mask;
89
90 STATIC volatile AO_t GC_stop_count = 0;
91                         /* Incremented at the beginning of GC_stop_world. */
92
93 STATIC volatile AO_t GC_world_is_stopped = FALSE;
94                         /* FALSE ==> it is safe for threads to restart, i.e. */
95                         /* they will see another suspend signal before they  */
96                         /* are expected to stop (unless they have voluntarily */
97                         /* stopped).                                         */
98
99 #ifdef GC_OSF1_THREADS
100   STATIC GC_bool GC_retry_signals = TRUE;
101 #else
102   STATIC GC_bool GC_retry_signals = FALSE;
103 #endif
104
105 /*
106  * We use signals to stop threads during GC.
107  *
108  * Suspended threads wait in signal handler for SIG_THR_RESTART.
109  * That's more portable than semaphores or condition variables.
110  * (We do use sem_post from a signal handler, but that should be portable.)
111  *
112  * The thread suspension signal SIG_SUSPEND is now defined in gc_priv.h.
113  * Note that we can't just stop a thread; we need it to save its stack
114  * pointer(s) and acknowledge.
115  */
116
117 #ifndef SIG_THR_RESTART
118 #  if defined(GC_HPUX_THREADS) || defined(GC_OSF1_THREADS) || defined(GC_NETBSD_THREADS)
119 #    ifdef _SIGRTMIN
120 #      define SIG_THR_RESTART _SIGRTMIN + 5
121 #    else
122 #      define SIG_THR_RESTART SIGRTMIN + 5
123 #    endif
124 #  else
125 #   define SIG_THR_RESTART SIGXCPU
126 #  endif
127 #endif
128
129 STATIC sem_t GC_suspend_ack_sem;
130
131 #ifdef GC_NETBSD_THREADS
132 # define GC_NETBSD_THREADS_WORKAROUND
133   /* It seems to be necessary to wait until threads have restarted.     */
134   /* But it is unclear why that is the case.                            */
135   STATIC sem_t GC_restart_ack_sem;
136 #endif
137
138 STATIC void GC_suspend_handler_inner(ptr_t sig_arg, void *context);
139
140 #if defined(IA64) || defined(HP_PA) || defined(M68K)
141 #ifdef SA_SIGINFO
142   /*ARGSUSED*/
143   STATIC void GC_suspend_handler(int sig, siginfo_t *info, void *context)
144 #else
145   STATIC void GC_suspend_handler(int sig)
146 #endif
147 {
148   int old_errno = errno;
149   GC_with_callee_saves_pushed(GC_suspend_handler_inner, (ptr_t)(word)sig);
150   errno = old_errno;
151 }
152 #else
153 /* We believe that in all other cases the full context is already       */
154 /* in the signal handler frame.                                         */
155 #ifdef SA_SIGINFO
156   STATIC void GC_suspend_handler(int sig, siginfo_t *info, void *context)
157 #else
158   STATIC void GC_suspend_handler(int sig)
159 #endif
160 {
161   int old_errno = errno;
162 # ifndef SA_SIGINFO
163     void *context = 0;
164 # endif
165   GC_suspend_handler_inner((ptr_t)(word)sig, context);
166   errno = old_errno;
167 }
168 #endif
169
170 /*ARGSUSED*/
171 STATIC void GC_suspend_handler_inner(ptr_t sig_arg, void *context)
172 {
173     int sig = (int)(word)sig_arg;
174     int dummy;
175     pthread_t my_thread = pthread_self();
176     GC_thread me;
177     IF_CANCEL(int cancel_state;)
178
179     AO_t my_stop_count = AO_load(&GC_stop_count);
180
181     if (sig != SIG_SUSPEND) ABORT("Bad signal in suspend_handler");
182
183     DISABLE_CANCEL(cancel_state);
184         /* pthread_setcancelstate is not defined to be async-signal-safe. */
185         /* But the glibc version appears to be in the absence of          */
186         /* asynchronous cancellation.  And since this signal handler      */
187         /* to block on sigsuspend, which is both async-signal-safe        */
188         /* and a cancellation point, there seems to be no obvious way     */
189         /* out of it.  In fact, it looks to me like an async-signal-safe  */
190         /* cancellation point is inherently a problem, unless there is    */
191         /* some way to disable cancellation in the handler.               */
192 #   ifdef DEBUG_THREADS
193       GC_printf("Suspending 0x%x\n", (unsigned)my_thread);
194 #   endif
195
196     me = GC_lookup_thread(my_thread);
197     /* The lookup here is safe, since I'm doing this on behalf  */
198     /* of a thread which holds the allocation lock in order     */
199     /* to stop the world.  Thus concurrent modification of the  */
200     /* data structure is impossible.                            */
201     if (me -> stop_info.last_stop_count == my_stop_count) {
202         /* Duplicate signal.  OK if we are retrying.    */
203         if (!GC_retry_signals) {
204             WARN("Duplicate suspend signal in thread %p\n", pthread_self());
205         }
206         RESTORE_CANCEL(cancel_state);
207         return;
208     }
209 #   ifdef SPARC
210         me -> stop_info.stack_ptr = GC_save_regs_in_stack();
211 #   else
212         me -> stop_info.stack_ptr = (ptr_t)(&dummy);
213 #   endif
214 #   ifdef IA64
215         me -> backing_store_ptr = GC_save_regs_in_stack();
216 #   endif
217
218     /* Tell the thread that wants to stop the world that this   */
219     /* thread has been stopped.  Note that sem_post() is        */
220     /* the only async-signal-safe primitive in LinuxThreads.    */
221     sem_post(&GC_suspend_ack_sem);
222     me -> stop_info.last_stop_count = my_stop_count;
223
224     /* Wait until that thread tells us to restart by sending    */
225     /* this thread a SIG_THR_RESTART signal.                    */
226     /* SIG_THR_RESTART should be masked at this point.  Thus there      */
227     /* is no race.                                              */
228     /* We do not continue until we receive a SIG_THR_RESTART,   */
229     /* but we do not take that as authoritative.  (We may be    */
230     /* accidentally restarted by one of the user signals we     */
231     /* don't block.)  After we receive the signal, we use a     */
232     /* primitive and expensive mechanism to wait until it's     */
233     /* really safe to proceed.  Under normal circumstances,     */
234     /* this code should not be executed.                        */
235     do {
236         sigsuspend (&suspend_handler_mask);
237     } while (AO_load_acquire(&GC_world_is_stopped)
238              && AO_load(&GC_stop_count) == my_stop_count);
239     /* If the RESTART signal gets lost, we can still lose.  That should be  */
240     /* less likely than losing the SUSPEND signal, since we don't do much   */
241     /* between the sem_post and sigsuspend.                                 */
242     /* We'd need more handshaking to work around that.                      */
243     /* Simply dropping the sigsuspend call should be safe, but is unlikely  */
244     /* to be efficient.                                                     */
245
246 #   ifdef DEBUG_THREADS
247       GC_printf("Continuing 0x%x\n", (unsigned)my_thread);
248 #   endif
249     RESTORE_CANCEL(cancel_state);
250 }
251
252 STATIC void GC_restart_handler(int sig)
253 {
254     if (sig != SIG_THR_RESTART) ABORT("Bad signal in suspend_handler");
255
256 #   ifdef GC_NETBSD_THREADS_WORKAROUND
257       sem_post(&GC_restart_ack_sem);
258 #   endif
259
260     /*
261     ** Note: even if we don't do anything useful here,
262     ** it would still be necessary to have a signal handler,
263     ** rather than ignoring the signals, otherwise
264     ** the signals will not be delivered at all, and
265     ** will thus not interrupt the sigsuspend() above.
266     */
267
268 #   ifdef DEBUG_THREADS
269       GC_printf("In GC_restart_handler for 0x%x\n", (unsigned)pthread_self());
270 #   endif
271 }
272
273 #endif /* !GC_OPENBSD_THREADS */
274
275 # ifdef IA64
276 #   define IF_IA64(x) x
277 # else
278 #   define IF_IA64(x)
279 # endif
280 /* We hold allocation lock.  Should do exactly the right thing if the   */
281 /* world is stopped.  Should not fail if it isn't.                      */
282 GC_INNER void GC_push_all_stacks(void)
283 {
284     GC_bool found_me = FALSE;
285     size_t nthreads = 0;
286     int i;
287     GC_thread p;
288     ptr_t lo, hi;
289     /* On IA64, we also need to scan the register backing store. */
290     IF_IA64(ptr_t bs_lo; ptr_t bs_hi;)
291     pthread_t me = pthread_self();
292     word total_size = 0;
293
294     if (!GC_thr_initialized) GC_thr_init();
295 #   ifdef DEBUG_THREADS
296       GC_printf("Pushing stacks from thread 0x%x\n", (unsigned) me);
297 #   endif
298     for (i = 0; i < THREAD_TABLE_SZ; i++) {
299       for (p = GC_threads[i]; p != 0; p = p -> next) {
300         if (p -> flags & FINISHED) continue;
301         ++nthreads;
302         if (THREAD_EQUAL(p -> id, me)) {
303             GC_ASSERT(!p->thread_blocked);
304 #           ifdef SPARC
305                 lo = (ptr_t)GC_save_regs_in_stack();
306 #           else
307                 lo = GC_approx_sp();
308 #           endif
309             found_me = TRUE;
310             IF_IA64(bs_hi = (ptr_t)GC_save_regs_in_stack();)
311         } else {
312             lo = p -> stop_info.stack_ptr;
313             IF_IA64(bs_hi = p -> backing_store_ptr;)
314         }
315         if ((p -> flags & MAIN_THREAD) == 0) {
316             hi = p -> stack_end;
317             IF_IA64(bs_lo = p -> backing_store_end);
318         } else {
319             /* The original stack. */
320             hi = GC_stackbottom;
321             IF_IA64(bs_lo = BACKING_STORE_BASE;)
322         }
323 #       ifdef DEBUG_THREADS
324           GC_printf("Stack for thread 0x%x = [%p,%p)\n",
325                     (unsigned)(p -> id), lo, hi);
326 #       endif
327         if (0 == lo) ABORT("GC_push_all_stacks: sp not set!\n");
328         GC_push_all_stack_frames(lo, hi, p -> activation_frame);
329 #       ifdef STACK_GROWS_UP
330           total_size += lo - hi;
331 #       else
332           total_size += hi - lo; /* lo <= hi */
333 #       endif
334 #       ifdef IA64
335 #         ifdef DEBUG_THREADS
336             GC_printf("Reg stack for thread 0x%x = [%p,%p)\n",
337                       (unsigned)p -> id, bs_lo, bs_hi);
338 #         endif
339           /* FIXME:  This (if p->id==me) may add an unbounded number of */
340           /* entries, and hence overflow the mark stack, which is bad.  */
341           GC_push_all_register_frames(bs_lo, bs_hi,
342                         THREAD_EQUAL(p -> id, me), p -> activation_frame);
343           total_size += bs_hi - bs_lo; /* bs_lo <= bs_hi */
344 #       endif
345       }
346     }
347     if (GC_print_stats == VERBOSE) {
348         GC_log_printf("Pushed %d thread stacks\n", (int)nthreads);
349     }
350     if (!found_me && !GC_in_thread_creation)
351       ABORT("Collecting from unknown thread.");
352     GC_total_stacksize = total_size;
353 }
354
355 /* There seems to be a very rare thread stopping problem.  To help us  */
356 /* debug that, we save the ids of the stopping thread. */
357 #ifdef DEBUG_THREADS
358   pthread_t GC_stopping_thread;
359   int GC_stopping_pid = 0;
360 #endif
361
362 /* We hold the allocation lock.  Suspend all threads that might */
363 /* still be running.  Return the number of suspend signals that */
364 /* were sent. */
365 STATIC int GC_suspend_all(void)
366 {
367     int n_live_threads = 0;
368     int i;
369     GC_thread p;
370 #   ifndef GC_OPENBSD_THREADS
371       int result;
372 #   endif
373     pthread_t my_thread = pthread_self();
374
375 #   ifdef DEBUG_THREADS
376       GC_stopping_thread = my_thread;
377       GC_stopping_pid = getpid();
378 #   endif
379     for (i = 0; i < THREAD_TABLE_SZ; i++) {
380       for (p = GC_threads[i]; p != 0; p = p -> next) {
381         if (!THREAD_EQUAL(p -> id, my_thread)) {
382             if (p -> flags & FINISHED) continue;
383             if (p -> thread_blocked) /* Will wait */ continue;
384 #           ifndef GC_OPENBSD_THREADS
385               if (p -> stop_info.last_stop_count == GC_stop_count) continue;
386               n_live_threads++;
387 #           endif
388 #           ifdef DEBUG_THREADS
389               GC_printf("Sending suspend signal to 0x%x\n",
390                         (unsigned)(p -> id));
391 #           endif
392
393 #           ifdef GC_OPENBSD_THREADS
394               if (pthread_suspend_np(p -> id) != 0)
395                 ABORT("pthread_suspend_np failed");
396               /* This will only work for userland pthreads.  It will    */
397               /* fail badly on rthreads.  Perhaps we should consider    */
398               /* a pthread_sp_np() function that returns the stack      */
399               /* pointer for a suspended thread and implement in both   */
400               /* pthreads and rthreads.                                 */
401               p -> stop_info.stack_ptr =
402                         *(ptr_t *)((char *)p -> id + UTHREAD_SP_OFFSET);
403 #           else
404               result = pthread_kill(p -> id, SIG_SUSPEND);
405               switch(result) {
406                 case ESRCH:
407                     /* Not really there anymore.  Possible? */
408                     n_live_threads--;
409                     break;
410                 case 0:
411                     break;
412                 default:
413                     ABORT("pthread_kill failed");
414               }
415 #           endif
416         }
417       }
418     }
419     return n_live_threads;
420 }
421
422 GC_INNER void GC_stop_world(void)
423 {
424     int i;
425 #   ifndef GC_OPENBSD_THREADS
426       int n_live_threads;
427       int code;
428 #   endif
429
430     GC_ASSERT(I_HOLD_LOCK());
431 #   ifdef DEBUG_THREADS
432       GC_printf("Stopping the world from 0x%x\n", (unsigned)pthread_self());
433 #   endif
434
435     /* Make sure all free list construction has stopped before we start. */
436     /* No new construction can start, since free list construction is   */
437     /* required to acquire and release the GC lock before it starts,    */
438     /* and we have the lock.                                            */
439 #   ifdef PARALLEL_MARK
440       if (GC_parallel) {
441         GC_acquire_mark_lock();
442         GC_ASSERT(GC_fl_builder_count == 0);
443         /* We should have previously waited for it to become zero. */
444       }
445 #   endif /* PARALLEL_MARK */
446
447 # ifdef GC_OPENBSD_THREADS
448     (void)GC_suspend_all();
449 # else
450     AO_store(&GC_stop_count, GC_stop_count+1);
451         /* Only concurrent reads are possible. */
452     AO_store_release(&GC_world_is_stopped, TRUE);
453     n_live_threads = GC_suspend_all();
454
455       if (GC_retry_signals) {
456           unsigned long wait_usecs = 0;  /* Total wait since retry.     */
457 #         define WAIT_UNIT 3000
458 #         define RETRY_INTERVAL 100000
459           for (;;) {
460               int ack_count;
461
462               sem_getvalue(&GC_suspend_ack_sem, &ack_count);
463               if (ack_count == n_live_threads) break;
464               if (wait_usecs > RETRY_INTERVAL) {
465                   int newly_sent = GC_suspend_all();
466
467                   if (GC_print_stats) {
468                       GC_log_printf("Resent %d signals after timeout\n",
469                                 newly_sent);
470                   }
471                   sem_getvalue(&GC_suspend_ack_sem, &ack_count);
472                   if (newly_sent < n_live_threads - ack_count) {
473                       WARN("Lost some threads during GC_stop_world?!\n",0);
474                       n_live_threads = ack_count + newly_sent;
475                   }
476                   wait_usecs = 0;
477               }
478               usleep(WAIT_UNIT);
479               wait_usecs += WAIT_UNIT;
480           }
481       }
482     for (i = 0; i < n_live_threads; i++) {
483         retry:
484           if (0 != (code = sem_wait(&GC_suspend_ack_sem))) {
485               /* On Linux, sem_wait is documented to always return zero.*/
486               /* But the documentation appears to be incorrect.         */
487               if (errno == EINTR) {
488                 /* Seems to happen with some versions of gdb.   */
489                 goto retry;
490               }
491               ABORT("sem_wait for handler failed");
492           }
493     }
494 # endif
495
496 #   ifdef PARALLEL_MARK
497       if (GC_parallel)
498         GC_release_mark_lock();
499 #   endif
500 #   ifdef DEBUG_THREADS
501       GC_printf("World stopped from 0x%x\n", (unsigned)pthread_self());
502       GC_stopping_thread = 0;
503 #   endif
504 }
505
506 /* Caller holds allocation lock, and has held it continuously since     */
507 /* the world stopped.                                                   */
508 GC_INNER void GC_start_world(void)
509 {
510     pthread_t my_thread = pthread_self();
511     register int i;
512     register GC_thread p;
513 #   ifndef GC_OPENBSD_THREADS
514       register int n_live_threads = 0;
515       register int result;
516 #   endif
517 #   ifdef GC_NETBSD_THREADS_WORKAROUND
518       int code;
519 #   endif
520
521 #   ifdef DEBUG_THREADS
522       GC_printf("World starting\n");
523 #   endif
524
525 #   ifndef GC_OPENBSD_THREADS
526       AO_store(&GC_world_is_stopped, FALSE);
527 #   endif
528     for (i = 0; i < THREAD_TABLE_SZ; i++) {
529       for (p = GC_threads[i]; p != 0; p = p -> next) {
530         if (!THREAD_EQUAL(p -> id, my_thread)) {
531             if (p -> flags & FINISHED) continue;
532             if (p -> thread_blocked) continue;
533 #           ifndef GC_OPENBSD_THREADS
534               n_live_threads++;
535 #           endif
536 #           ifdef DEBUG_THREADS
537               GC_printf("Sending restart signal to 0x%x\n",
538                         (unsigned)(p -> id));
539 #           endif
540
541 #         ifdef GC_OPENBSD_THREADS
542             if (pthread_resume_np(p -> id) != 0)
543               ABORT("pthread_kill failed");
544 #         else
545             result = pthread_kill(p -> id, SIG_THR_RESTART);
546             switch(result) {
547                 case ESRCH:
548                     /* Not really there anymore.  Possible? */
549                     n_live_threads--;
550                     break;
551                 case 0:
552                     break;
553                 default:
554                     ABORT("pthread_kill failed");
555             }
556 #         endif
557         }
558       }
559     }
560 #   ifdef GC_NETBSD_THREADS_WORKAROUND
561       for (i = 0; i < n_live_threads; i++)
562         while (0 != (code = sem_wait(&GC_restart_ack_sem)))
563             if (errno != EINTR) {
564                 GC_err_printf("sem_wait() returned %d\n",
565                                code);
566                 ABORT("sem_wait() for restart handler failed");
567             }
568 #    endif
569 #    ifdef DEBUG_THREADS
570        GC_printf("World started\n");
571 #    endif
572 }
573
574 GC_INNER void GC_stop_init(void)
575 {
576 # ifndef GC_OPENBSD_THREADS
577     struct sigaction act;
578
579     if (sem_init(&GC_suspend_ack_sem, 0, 0) != 0)
580         ABORT("sem_init failed");
581 #   ifdef GC_NETBSD_THREADS_WORKAROUND
582       if (sem_init(&GC_restart_ack_sem, 0, 0) != 0)
583         ABORT("sem_init failed");
584 #   endif
585
586     act.sa_flags = SA_RESTART
587 #   ifdef SA_SIGINFO
588         | SA_SIGINFO
589 #   endif
590         ;
591     if (sigfillset(&act.sa_mask) != 0) {
592         ABORT("sigfillset() failed");
593     }
594     GC_remove_allowed_signals(&act.sa_mask);
595     /* SIG_THR_RESTART is set in the resulting mask.            */
596     /* It is unmasked by the handler when necessary.            */
597 #   ifdef SA_SIGINFO
598       act.sa_sigaction = GC_suspend_handler;
599 #   else
600       act.sa_handler = GC_suspend_handler;
601 #   endif
602     if (sigaction(SIG_SUSPEND, &act, NULL) != 0) {
603         ABORT("Cannot set SIG_SUSPEND handler");
604     }
605
606 #   ifdef SA_SIGINFO
607       act.sa_flags &= ~ SA_SIGINFO;
608 #   endif
609     act.sa_handler = GC_restart_handler;
610     if (sigaction(SIG_THR_RESTART, &act, NULL) != 0) {
611         ABORT("Cannot set SIG_THR_RESTART handler");
612     }
613
614     /* Initialize suspend_handler_mask. It excludes SIG_THR_RESTART. */
615       if (sigfillset(&suspend_handler_mask) != 0) ABORT("sigfillset() failed");
616       GC_remove_allowed_signals(&suspend_handler_mask);
617       if (sigdelset(&suspend_handler_mask, SIG_THR_RESTART) != 0)
618           ABORT("sigdelset() failed");
619
620     /* Check for GC_RETRY_SIGNALS.      */
621       if (0 != GETENV("GC_RETRY_SIGNALS")) {
622           GC_retry_signals = TRUE;
623       }
624       if (0 != GETENV("GC_NO_RETRY_SIGNALS")) {
625           GC_retry_signals = FALSE;
626       }
627       if (GC_print_stats && GC_retry_signals) {
628           GC_log_printf("Will retry suspend signal if necessary.\n");
629       }
630 # endif /* !GC_OPENBSD_THREADS */
631 }
632
633 #endif