mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-06-26 22:38:30 +00:00
Clean up some sleep code
This commit is contained in:
parent
9849b4c7ba
commit
672ccda37c
35 changed files with 310 additions and 598 deletions
6
third_party/nsync/testing/array.h → third_party/nsync/array.internal.h
vendored
Normal file → Executable file
6
third_party/nsync/testing/array.h → third_party/nsync/array.internal.h
vendored
Normal file → Executable file
|
@ -1,5 +1,5 @@
|
|||
#ifndef NSYNC_TESTING_ARRAY_H_
|
||||
#define NSYNC_TESTING_ARRAY_H_
|
||||
#ifndef NSYNC_ARRAY_H_
|
||||
#define NSYNC_ARRAY_H_
|
||||
/* clang-format off */
|
||||
|
||||
/* Return the number of elements in a C array a.
|
||||
|
@ -58,4 +58,4 @@ struct a_hdr_ {
|
|||
(a)->a_ = NULL; (a)->h_.max_ = 0; (a)->h_.len_ = 0; \
|
||||
} while (0)
|
||||
|
||||
#endif /*NSYNC_TESTING_ARRAY_H_*/
|
||||
#endif /*NSYNC_ARRAY_H_*/
|
8
third_party/nsync/compat.S
vendored
8
third_party/nsync/compat.S
vendored
|
@ -41,3 +41,11 @@ nsync_time_ms:
|
|||
nsync_time_us:
|
||||
jmp _timespec_frommicros
|
||||
.endfn nsync_time_us,globl
|
||||
|
||||
nsync_time_sleep:
|
||||
jmp _timespec_sleep
|
||||
.endfn nsync_time_us,globl
|
||||
|
||||
nsync_time_sleep_until:
|
||||
jmp _timespec_sleep_until
|
||||
.endfn nsync_time_us,globl
|
||||
|
|
145
third_party/nsync/futex.c
vendored
145
third_party/nsync/futex.c
vendored
|
@ -17,7 +17,9 @@
|
|||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/atomic.h"
|
||||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/sig.internal.h"
|
||||
#include "libc/calls/state.internal.h"
|
||||
#include "libc/calls/struct/timespec.h"
|
||||
#include "libc/calls/struct/timespec.internal.h"
|
||||
#include "libc/dce.h"
|
||||
|
@ -33,8 +35,10 @@
|
|||
#include "libc/sysv/consts/timer.h"
|
||||
#include "libc/thread/freebsd.internal.h"
|
||||
#include "libc/thread/thread.h"
|
||||
#include "libc/thread/tls.h"
|
||||
#include "third_party/nsync/atomic.h"
|
||||
#include "third_party/nsync/common.internal.h"
|
||||
#include "third_party/nsync/mu_semaphore.internal.h"
|
||||
#include "third_party/nsync/time.h"
|
||||
// clang-format off
|
||||
|
||||
/* futex() polyfill w/ sched_yield() fallback */
|
||||
|
@ -51,15 +55,15 @@ bool FUTEX_TIMEOUT_IS_ABSOLUTE;
|
|||
__attribute__((__constructor__)) static void nsync_futex_init_ (void) {
|
||||
int x = 0;
|
||||
|
||||
if (NSYNC_FUTEX_WIN32 && IsWindows ()) {
|
||||
FUTEX_WAIT_ = FUTEX_WAIT;
|
||||
|
||||
if (IsWindows ()) {
|
||||
FUTEX_IS_SUPPORTED = true;
|
||||
FUTEX_WAIT_ = FUTEX_WAIT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsFreebsd ()) {
|
||||
FUTEX_IS_SUPPORTED = true;
|
||||
FUTEX_WAIT_ = FUTEX_WAIT;
|
||||
FUTEX_TIMEOUT_IS_ABSOLUTE = true;
|
||||
FUTEX_PRIVATE_FLAG_ = FUTEX_PRIVATE_FLAG;
|
||||
return;
|
||||
|
@ -105,44 +109,41 @@ __attribute__((__constructor__)) static void nsync_futex_init_ (void) {
|
|||
}
|
||||
|
||||
static int nsync_futex_polyfill_ (int *p, int expect, struct timespec *timeout) {
|
||||
int rc;
|
||||
atomic_int *w;
|
||||
int64_t max, nanos;
|
||||
int64_t nanos, maxnanos;
|
||||
nsync_atomic_uint32_ *w;
|
||||
struct timespec ts, deadline;
|
||||
|
||||
w = (atomic_int *)p;
|
||||
if (atomic_load_explicit (p, memory_order_relaxed) != expect) {
|
||||
w = (nsync_atomic_uint32_ *)p;
|
||||
if (ATM_LOAD (p) != expect) {
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
ts = _timespec_real ();
|
||||
ts = nsync_time_now ();
|
||||
if (!timeout) {
|
||||
deadline = nsync_time_no_deadline;
|
||||
} else if (FUTEX_TIMEOUT_IS_ABSOLUTE) {
|
||||
deadline = *timeout;
|
||||
} else {
|
||||
deadline = _timespec_add (ts, *timeout);
|
||||
deadline = nsync_time_add (ts, *timeout);
|
||||
}
|
||||
|
||||
nanos = 100;
|
||||
max = __SIG_POLLING_INTERVAL_MS * 1000L * 1000;
|
||||
while (_timespec_gt (deadline, ts)) {
|
||||
if (atomic_load_explicit (p, memory_order_relaxed) != expect) {
|
||||
maxnanos = __SIG_POLLING_INTERVAL_MS * 1000L * 1000;
|
||||
while (nsync_time_cmp (deadline, ts) > 0) {
|
||||
if (ATM_LOAD (p) != expect) {
|
||||
return 0;
|
||||
}
|
||||
ts = _timespec_add (ts, _timespec_fromnanos (nanos));
|
||||
if (_timespec_gt (ts, deadline)) {
|
||||
ts = nsync_time_add (ts, _timespec_fromnanos (nanos));
|
||||
if (nsync_time_cmp (ts, deadline) > 0) {
|
||||
ts = deadline;
|
||||
}
|
||||
rc = clock_nanosleep (CLOCK_REALTIME, TIMER_ABSTIME, &ts, 0);
|
||||
if (rc) {
|
||||
ASSERT (rc == EINTR);
|
||||
if (nsync_time_sleep_until (ts)) {
|
||||
return -EINTR;
|
||||
}
|
||||
if (nanos < max) {
|
||||
if (nanos < maxnanos) {
|
||||
nanos <<= 1;
|
||||
if (nanos > max) {
|
||||
nanos = max;
|
||||
if (nanos > maxnanos) {
|
||||
nanos = maxnanos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,38 +155,42 @@ int nsync_futex_wait_ (int *p, int expect, char pshare, struct timespec *timeout
|
|||
uint32_t ms;
|
||||
int rc, op, fop;
|
||||
|
||||
if (!FUTEX_IS_SUPPORTED) {
|
||||
return nsync_futex_polyfill_ (p, expect, timeout);
|
||||
}
|
||||
|
||||
op = FUTEX_WAIT_;
|
||||
if (pshare == PTHREAD_PROCESS_PRIVATE) {
|
||||
op |= FUTEX_PRIVATE_FLAG_;
|
||||
}
|
||||
|
||||
if (NSYNC_FUTEX_WIN32 && IsWindows ()) {
|
||||
// Windows 8 futexes don't support multiple processes :(
|
||||
if (pshare) {
|
||||
return nsync_futex_polyfill_ (p, expect, timeout);
|
||||
}
|
||||
if (timeout) {
|
||||
ms = _timespec_tomillis (*timeout);
|
||||
if (FUTEX_IS_SUPPORTED) {
|
||||
if (IsWindows ()) {
|
||||
// Windows 8 futexes don't support multiple processes :(
|
||||
if (pshare) {
|
||||
goto Polyfill;
|
||||
}
|
||||
if (timeout) {
|
||||
ms = _timespec_tomillis (*timeout);
|
||||
} else {
|
||||
ms = -1;
|
||||
}
|
||||
if (WaitOnAddress (p, &expect, sizeof(int), ms)) {
|
||||
rc = 0;
|
||||
} else {
|
||||
rc = -GetLastError ();
|
||||
}
|
||||
} else if (IsFreebsd ()) {
|
||||
rc = sys_umtx_timedwait_uint (
|
||||
p, expect, pshare, timeout);
|
||||
} else {
|
||||
ms = -1;
|
||||
rc = _futex (p, op, expect, timeout, 0,
|
||||
FUTEX_WAIT_BITS_);
|
||||
if (IsOpenbsd() && rc > 0) {
|
||||
rc = -rc;
|
||||
}
|
||||
}
|
||||
if (WaitOnAddress (p, &expect, sizeof(int), ms)) {
|
||||
rc = 0;
|
||||
} else {
|
||||
rc = -GetLastError ();
|
||||
}
|
||||
} else if (IsFreebsd ()) {
|
||||
rc = sys_umtx_timedwait_uint (p, expect, pshare, timeout);
|
||||
} else {
|
||||
rc = _futex (p, op, expect, timeout, 0, FUTEX_WAIT_BITS_);
|
||||
if (IsOpenbsd() && rc > 0) {
|
||||
// [jart] openbsd does this without setting carry flag
|
||||
rc = -rc;
|
||||
}
|
||||
Polyfill:
|
||||
__get_tls()->tib_flags |= TIB_FLAG_TIME_CRITICAL;
|
||||
rc = nsync_futex_polyfill_ (p, expect, timeout);
|
||||
__get_tls()->tib_flags &= ~TIB_FLAG_TIME_CRITICAL;
|
||||
}
|
||||
|
||||
STRACE ("futex(%t, %s, %d, %s) → %s",
|
||||
|
@ -202,40 +207,40 @@ int nsync_futex_wake_ (int *p, int count, char pshare) {
|
|||
|
||||
ASSERT (count == 1 || count == INT_MAX);
|
||||
|
||||
if (!FUTEX_IS_SUPPORTED) {
|
||||
nsync_yield_ ();
|
||||
return 0;
|
||||
}
|
||||
|
||||
op = FUTEX_WAKE;
|
||||
if (pshare == PTHREAD_PROCESS_PRIVATE) {
|
||||
op |= FUTEX_PRIVATE_FLAG_;
|
||||
}
|
||||
|
||||
if (NSYNC_FUTEX_WIN32 && IsWindows ()) {
|
||||
if (pshare) {
|
||||
nsync_yield_ ();
|
||||
return 0;
|
||||
}
|
||||
if (count == 1) {
|
||||
WakeByAddressSingle (p);
|
||||
if (FUTEX_IS_SUPPORTED) {
|
||||
if (IsWindows ()) {
|
||||
if (pshare) {
|
||||
goto Polyfill;
|
||||
}
|
||||
if (count == 1) {
|
||||
WakeByAddressSingle (p);
|
||||
} else {
|
||||
WakeByAddressAll (p);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (IsFreebsd ()) {
|
||||
if (pshare) {
|
||||
fop = UMTX_OP_WAKE;
|
||||
} else {
|
||||
fop = UMTX_OP_WAKE_PRIVATE;
|
||||
}
|
||||
rc = sys_umtx_op (p, fop, count, 0, 0);
|
||||
} else {
|
||||
WakeByAddressAll (p);
|
||||
rc = wake (p, op, count);
|
||||
}
|
||||
rc = 0;
|
||||
} else if (IsFreebsd ()) {
|
||||
if (pshare) {
|
||||
fop = UMTX_OP_WAKE;
|
||||
} else {
|
||||
fop = UMTX_OP_WAKE_PRIVATE;
|
||||
}
|
||||
rc = sys_umtx_op (p, fop, count, 0, 0);
|
||||
} else {
|
||||
rc = wake (p, op, count);
|
||||
Polyfill:
|
||||
sched_yield ();
|
||||
rc = 0;
|
||||
}
|
||||
|
||||
STRACE ("futex(%t, %s, %d) → %s", p,
|
||||
DescribeFutexOp(op),
|
||||
STRACE ("futex(%t, %s, %d) → %s",
|
||||
p, DescribeFutexOp(op),
|
||||
count, DescribeErrnoResult(rc));
|
||||
|
||||
return rc;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/mem/mem.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "third_party/nsync/testing/array.h"
|
||||
#include "third_party/nsync/array.internal.h"
|
||||
// clang-format off
|
||||
|
||||
void a_ensure_ (void *v, int delta, int sz) {
|
132
third_party/nsync/mu_semaphore.c
vendored
132
third_party/nsync/mu_semaphore.c
vendored
|
@ -1,56 +1,128 @@
|
|||
/*-*- mode:c;indent-tabs-mode:t;c-basic-offset:8;tab-width:8;coding:utf-8 -*-│
|
||||
│vi: set et ft=c ts=8 tw=8 fenc=utf-8 :vi│
|
||||
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||
│ Copyright 2022 Justine Alexandra Roberts Tunney │
|
||||
│ Copyright 2016 Google Inc. │
|
||||
│ │
|
||||
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||
│ any purpose with or without fee is hereby granted, provided that the │
|
||||
│ above copyright notice and this permission notice appear in all copies. │
|
||||
│ Licensed under the Apache License, Version 2.0 (the "License"); │
|
||||
│ you may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/dce.h"
|
||||
#include "libc/assert.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "libc/thread/thread.h"
|
||||
#include "third_party/nsync/atomic.h"
|
||||
#include "third_party/nsync/atomic.internal.h"
|
||||
#include "third_party/nsync/futex.internal.h"
|
||||
#include "third_party/nsync/mu_semaphore.h"
|
||||
#include "third_party/nsync/mu_semaphore.internal.h"
|
||||
|
||||
asm(".ident\t\"\\n\\n\
|
||||
*NSYNC (Apache 2.0)\\n\
|
||||
Copyright 2016 Google, Inc.\\n\
|
||||
https://github.com/google/nsync\"");
|
||||
// clang-format off
|
||||
|
||||
#ifdef TINY
|
||||
#define ASSERT(x) _unassert(x)
|
||||
#else
|
||||
#define ASSERT(x) _npassert(x)
|
||||
#endif
|
||||
|
||||
/* Check that atomic operations on nsync_atomic_uint32_ can be applied to int. */
|
||||
static const int assert_int_size = 1 /
|
||||
(sizeof (assert_int_size) == sizeof (uint32_t) &&
|
||||
sizeof (nsync_atomic_uint32_) == sizeof (uint32_t));
|
||||
|
||||
struct futex {
|
||||
int i; /* lo half=count; hi half=waiter count */
|
||||
};
|
||||
|
||||
static nsync_semaphore *sem_big_enough_for_futex = (nsync_semaphore *) (uintptr_t)(1 /
|
||||
(sizeof (struct futex) <= sizeof (*sem_big_enough_for_futex)));
|
||||
|
||||
/* Initialize *s; the initial value is 0. */
|
||||
void nsync_mu_semaphore_init (nsync_semaphore *s) {
|
||||
if (NSYNC_FUTEX_WIN32 || !IsWindows ())
|
||||
nsync_mu_semaphore_init_futex (s);
|
||||
else
|
||||
nsync_mu_semaphore_init_win32 (s);
|
||||
struct futex *f = (struct futex *) s;
|
||||
f->i = 0;
|
||||
}
|
||||
|
||||
/* Wait until the count of *s exceeds 0, and decrement it. */
|
||||
void nsync_mu_semaphore_p (nsync_semaphore *s) {
|
||||
if (NSYNC_FUTEX_WIN32 || !IsWindows ())
|
||||
nsync_mu_semaphore_p_futex (s);
|
||||
else
|
||||
nsync_mu_semaphore_p_win32 (s);
|
||||
struct futex *f = (struct futex *) s;
|
||||
int i;
|
||||
do {
|
||||
i = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
if (i == 0) {
|
||||
int futex_result = nsync_futex_wait_ (&f->i, i, PTHREAD_PROCESS_PRIVATE, NULL);
|
||||
ASSERT (futex_result == 0 ||
|
||||
futex_result == -EINTR ||
|
||||
futex_result == -EWOULDBLOCK);
|
||||
}
|
||||
} while (i == 0 || !ATM_CAS_ACQ ((nsync_atomic_uint32_ *) &f->i, i, i-1));
|
||||
}
|
||||
|
||||
/* Wait until one of:
|
||||
the count of *s is non-zero, in which case decrement *s and return 0;
|
||||
or abs_deadline expires, in which case return ETIMEDOUT. */
|
||||
int nsync_mu_semaphore_p_with_deadline (nsync_semaphore *s, nsync_time abs_deadline) {
|
||||
if (NSYNC_FUTEX_WIN32 || !IsWindows ())
|
||||
return nsync_mu_semaphore_p_with_deadline_futex (s, abs_deadline);
|
||||
else
|
||||
return nsync_mu_semaphore_p_with_deadline_win32 (s, abs_deadline);
|
||||
struct futex *f = (struct futex *)s;
|
||||
int i;
|
||||
int result = 0;
|
||||
do {
|
||||
i = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
if (i == 0) {
|
||||
int futex_result;
|
||||
struct timespec ts_buf;
|
||||
const struct timespec *ts = NULL;
|
||||
if (nsync_time_cmp (abs_deadline, nsync_time_no_deadline) != 0) {
|
||||
memset (&ts_buf, 0, sizeof (ts_buf));
|
||||
if (FUTEX_TIMEOUT_IS_ABSOLUTE) {
|
||||
ts_buf.tv_sec = NSYNC_TIME_SEC (abs_deadline);
|
||||
ts_buf.tv_nsec = NSYNC_TIME_NSEC (abs_deadline);
|
||||
} else {
|
||||
nsync_time now;
|
||||
now = nsync_time_now ();
|
||||
if (nsync_time_cmp (now, abs_deadline) > 0) {
|
||||
ts_buf.tv_sec = 0;
|
||||
ts_buf.tv_nsec = 0;
|
||||
} else {
|
||||
nsync_time rel_deadline;
|
||||
rel_deadline = nsync_time_sub (abs_deadline, now);
|
||||
ts_buf.tv_sec = NSYNC_TIME_SEC (rel_deadline);
|
||||
ts_buf.tv_nsec = NSYNC_TIME_NSEC (rel_deadline);
|
||||
}
|
||||
}
|
||||
ts = &ts_buf;
|
||||
}
|
||||
futex_result = nsync_futex_wait_ (&f->i, i, PTHREAD_PROCESS_PRIVATE, ts);
|
||||
ASSERT (futex_result == 0 ||
|
||||
futex_result == -EINTR ||
|
||||
futex_result == -ETIMEDOUT ||
|
||||
futex_result == -EWOULDBLOCK);
|
||||
/* Some systems don't wait as long as they are told. */
|
||||
if (futex_result == -ETIMEDOUT &&
|
||||
nsync_time_cmp (abs_deadline, nsync_time_now ()) <= 0) {
|
||||
result = ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
} while (result == 0 && (i == 0 || !ATM_CAS_ACQ ((nsync_atomic_uint32_ *) &f->i, i, i - 1)));
|
||||
return (result);
|
||||
}
|
||||
|
||||
/* Ensure that the count of *s is at least 1. */
|
||||
void nsync_mu_semaphore_v (nsync_semaphore *s) {
|
||||
if (NSYNC_FUTEX_WIN32 || !IsWindows ())
|
||||
nsync_mu_semaphore_v_futex (s);
|
||||
else
|
||||
nsync_mu_semaphore_v_win32 (s);
|
||||
struct futex *f = (struct futex *) s;
|
||||
uint32_t old_value;
|
||||
do {
|
||||
old_value = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
} while (!ATM_CAS_REL ((nsync_atomic_uint32_ *) &f->i, old_value, old_value+1));
|
||||
ASSERT (nsync_futex_wake_ (&f->i, 1, PTHREAD_PROCESS_PRIVATE) >= 0);
|
||||
}
|
||||
|
|
25
third_party/nsync/mu_semaphore.internal.h
vendored
Normal file → Executable file
25
third_party/nsync/mu_semaphore.internal.h
vendored
Normal file → Executable file
|
@ -1,25 +0,0 @@
|
|||
#ifndef NSYNC_MU_SEMAPHORE_INTERNAL_H_
|
||||
#define NSYNC_MU_SEMAPHORE_INTERNAL_H_
|
||||
#include "third_party/nsync/mu_semaphore.h"
|
||||
#include "third_party/nsync/time.h"
|
||||
|
||||
#ifndef NSYNC_FUTEX_WIN32
|
||||
#define NSYNC_FUTEX_WIN32 1
|
||||
#endif
|
||||
|
||||
#if !(__ASSEMBLER__ + __LINKER__ + 0)
|
||||
COSMOPOLITAN_C_START_
|
||||
|
||||
void nsync_mu_semaphore_init_futex(nsync_semaphore *);
|
||||
void nsync_mu_semaphore_p_futex(nsync_semaphore *);
|
||||
int nsync_mu_semaphore_p_with_deadline_futex(nsync_semaphore *, nsync_time);
|
||||
void nsync_mu_semaphore_v_futex(nsync_semaphore *);
|
||||
|
||||
void nsync_mu_semaphore_init_win32(nsync_semaphore *);
|
||||
void nsync_mu_semaphore_p_win32(nsync_semaphore *);
|
||||
int nsync_mu_semaphore_p_with_deadline_win32(nsync_semaphore *, nsync_time);
|
||||
void nsync_mu_semaphore_v_win32(nsync_semaphore *);
|
||||
|
||||
COSMOPOLITAN_C_END_
|
||||
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
|
||||
#endif /* NSYNC_MU_SEMAPHORE_INTERNAL_H_ */
|
129
third_party/nsync/mu_semaphore_futex.c
vendored
129
third_party/nsync/mu_semaphore_futex.c
vendored
|
@ -1,129 +0,0 @@
|
|||
/*-*- mode:c;indent-tabs-mode:t;c-basic-offset:8;tab-width:8;coding:utf-8 -*-│
|
||||
│vi: set et ft=c ts=8 tw=8 fenc=utf-8 :vi│
|
||||
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||
│ Copyright 2016 Google Inc. │
|
||||
│ │
|
||||
│ Licensed under the Apache License, Version 2.0 (the "License"); │
|
||||
│ you may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/assert.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "libc/thread/thread.h"
|
||||
#include "third_party/nsync/atomic.h"
|
||||
#include "third_party/nsync/atomic.internal.h"
|
||||
#include "third_party/nsync/futex.internal.h"
|
||||
#include "third_party/nsync/mu_semaphore.h"
|
||||
#include "third_party/nsync/mu_semaphore.internal.h"
|
||||
|
||||
asm(".ident\t\"\\n\\n\
|
||||
*NSYNC (Apache 2.0)\\n\
|
||||
Copyright 2016 Google, Inc.\\n\
|
||||
https://github.com/google/nsync\"");
|
||||
// clang-format off
|
||||
|
||||
#ifdef TINY
|
||||
#define ASSERT(x) _unassert(x)
|
||||
#else
|
||||
#define ASSERT(x) _npassert(x)
|
||||
#endif
|
||||
|
||||
/* Check that atomic operations on nsync_atomic_uint32_ can be applied to int. */
|
||||
static const int assert_int_size = 1 /
|
||||
(sizeof (assert_int_size) == sizeof (uint32_t) &&
|
||||
sizeof (nsync_atomic_uint32_) == sizeof (uint32_t));
|
||||
|
||||
struct futex {
|
||||
int i; /* lo half=count; hi half=waiter count */
|
||||
};
|
||||
|
||||
static nsync_semaphore *sem_big_enough_for_futex = (nsync_semaphore *) (uintptr_t)(1 /
|
||||
(sizeof (struct futex) <= sizeof (*sem_big_enough_for_futex)));
|
||||
|
||||
/* Initialize *s; the initial value is 0. */
|
||||
void nsync_mu_semaphore_init_futex (nsync_semaphore *s) {
|
||||
struct futex *f = (struct futex *) s;
|
||||
f->i = 0;
|
||||
}
|
||||
|
||||
/* Wait until the count of *s exceeds 0, and decrement it. */
|
||||
void nsync_mu_semaphore_p_futex (nsync_semaphore *s) {
|
||||
struct futex *f = (struct futex *) s;
|
||||
int i;
|
||||
do {
|
||||
i = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
if (i == 0) {
|
||||
int futex_result = nsync_futex_wait_ (&f->i, i, PTHREAD_PROCESS_PRIVATE, NULL);
|
||||
ASSERT (futex_result == 0 ||
|
||||
futex_result == -EINTR ||
|
||||
futex_result == -EWOULDBLOCK);
|
||||
}
|
||||
} while (i == 0 || !ATM_CAS_ACQ ((nsync_atomic_uint32_ *) &f->i, i, i-1));
|
||||
}
|
||||
|
||||
/* Wait until one of:
|
||||
the count of *s is non-zero, in which case decrement *s and return 0;
|
||||
or abs_deadline expires, in which case return ETIMEDOUT. */
|
||||
int nsync_mu_semaphore_p_with_deadline_futex (nsync_semaphore *s, nsync_time abs_deadline) {
|
||||
struct futex *f = (struct futex *)s;
|
||||
int i;
|
||||
int result = 0;
|
||||
do {
|
||||
i = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
if (i == 0) {
|
||||
int futex_result;
|
||||
struct timespec ts_buf;
|
||||
const struct timespec *ts = NULL;
|
||||
if (nsync_time_cmp (abs_deadline, nsync_time_no_deadline) != 0) {
|
||||
memset (&ts_buf, 0, sizeof (ts_buf));
|
||||
if (FUTEX_TIMEOUT_IS_ABSOLUTE) {
|
||||
ts_buf.tv_sec = NSYNC_TIME_SEC (abs_deadline);
|
||||
ts_buf.tv_nsec = NSYNC_TIME_NSEC (abs_deadline);
|
||||
} else {
|
||||
nsync_time now;
|
||||
now = nsync_time_now ();
|
||||
if (nsync_time_cmp (now, abs_deadline) > 0) {
|
||||
ts_buf.tv_sec = 0;
|
||||
ts_buf.tv_nsec = 0;
|
||||
} else {
|
||||
nsync_time rel_deadline;
|
||||
rel_deadline = nsync_time_sub (abs_deadline, now);
|
||||
ts_buf.tv_sec = NSYNC_TIME_SEC (rel_deadline);
|
||||
ts_buf.tv_nsec = NSYNC_TIME_NSEC (rel_deadline);
|
||||
}
|
||||
}
|
||||
ts = &ts_buf;
|
||||
}
|
||||
futex_result = nsync_futex_wait_ (&f->i, i, PTHREAD_PROCESS_PRIVATE, ts);
|
||||
ASSERT (futex_result == 0 ||
|
||||
futex_result == -EINTR ||
|
||||
futex_result == -ETIMEDOUT ||
|
||||
futex_result == -EWOULDBLOCK);
|
||||
/* Some systems don't wait as long as they are told. */
|
||||
if (futex_result == -ETIMEDOUT &&
|
||||
nsync_time_cmp (abs_deadline, nsync_time_now ()) <= 0) {
|
||||
result = ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
} while (result == 0 && (i == 0 || !ATM_CAS_ACQ ((nsync_atomic_uint32_ *) &f->i, i, i - 1)));
|
||||
return (result);
|
||||
}
|
||||
|
||||
/* Ensure that the count of *s is at least 1. */
|
||||
void nsync_mu_semaphore_v_futex (nsync_semaphore *s) {
|
||||
struct futex *f = (struct futex *) s;
|
||||
uint32_t old_value;
|
||||
do {
|
||||
old_value = ATM_LOAD ((nsync_atomic_uint32_ *) &f->i);
|
||||
} while (!ATM_CAS_REL ((nsync_atomic_uint32_ *) &f->i, old_value, old_value+1));
|
||||
ASSERT (nsync_futex_wake_ (&f->i, 1, PTHREAD_PROCESS_PRIVATE) >= 0);
|
||||
}
|
85
third_party/nsync/mu_semaphore_win32.c
vendored
85
third_party/nsync/mu_semaphore_win32.c
vendored
|
@ -1,85 +0,0 @@
|
|||
/*-*- mode:c;indent-tabs-mode:t;c-basic-offset:8;tab-width:8;coding:utf-8 -*-│
|
||||
│vi: set et ft=c ts=8 tw=8 fenc=utf-8 :vi│
|
||||
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||
│ Copyright 2016 Google Inc. │
|
||||
│ │
|
||||
│ Licensed under the Apache License, Version 2.0 (the "License"); │
|
||||
│ you may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/calls/state.internal.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/nt/enum/wait.h"
|
||||
#include "libc/nt/synchronization.h"
|
||||
#include "libc/runtime/runtime.h"
|
||||
#include "third_party/nsync/mu_semaphore.h"
|
||||
#include "third_party/nsync/mu_semaphore.internal.h"
|
||||
#include "third_party/nsync/time.h"
|
||||
|
||||
asm(".ident\t\"\\n\\n\
|
||||
*NSYNC (Apache 2.0)\\n\
|
||||
Copyright 2016 Google, Inc.\\n\
|
||||
https://github.com/google/nsync\"");
|
||||
// clang-format off
|
||||
|
||||
/* Initialize *s; the initial value is 0. */
|
||||
void nsync_mu_semaphore_init_win32 (nsync_semaphore *s) {
|
||||
int64_t *h = (int64_t *) s;
|
||||
*h = CreateSemaphore (&kNtIsInheritable, 0, 1, NULL);
|
||||
if (!*h) notpossible;
|
||||
}
|
||||
|
||||
/* Wait until the count of *s exceeds 0, and decrement it. */
|
||||
void nsync_mu_semaphore_p_win32 (nsync_semaphore *s) {
|
||||
int64_t *h = (int64_t *) s;
|
||||
WaitForSingleObject (*h, -1u);
|
||||
}
|
||||
|
||||
/* Wait until one of:
|
||||
the count of *s is non-zero, in which case decrement *s and return 0;
|
||||
or abs_deadline expires, in which case return ETIMEDOUT. */
|
||||
int nsync_mu_semaphore_p_with_deadline_win32 (nsync_semaphore *s, nsync_time abs_deadline) {
|
||||
int64_t *h = (int64_t *) s;
|
||||
int result;
|
||||
|
||||
if (nsync_time_cmp (abs_deadline, nsync_time_no_deadline) == 0) {
|
||||
result = WaitForSingleObject (*h, -1u);
|
||||
} else {
|
||||
nsync_time now;
|
||||
now = nsync_time_now ();
|
||||
do {
|
||||
if (nsync_time_cmp (abs_deadline, now) <= 0) {
|
||||
result = WaitForSingleObject (*h, 0);
|
||||
} else {
|
||||
nsync_time delay;
|
||||
delay = nsync_time_sub (abs_deadline, now);
|
||||
if (NSYNC_TIME_SEC (delay) > 1000*1000) {
|
||||
result = WaitForSingleObject (*h, 1000*1000);
|
||||
} else {
|
||||
result = WaitForSingleObject (*h,
|
||||
(unsigned) (NSYNC_TIME_SEC (delay) * 1000 +
|
||||
(NSYNC_TIME_NSEC (delay) + 999999) / (1000 * 1000)));
|
||||
}
|
||||
}
|
||||
if (result == kNtWaitTimeout) {
|
||||
now = nsync_time_now ();
|
||||
}
|
||||
} while (result == kNtWaitTimeout && /* Windows generates early wakeups. */
|
||||
nsync_time_cmp (abs_deadline, now) > 0);
|
||||
}
|
||||
return (result == kNtWaitTimeout ? ETIMEDOUT : 0);
|
||||
}
|
||||
|
||||
/* Ensure that the count of *s is at least 1. */
|
||||
void nsync_mu_semaphore_v_win32 (nsync_semaphore *s) {
|
||||
int64_t *h = (int64_t *) s;
|
||||
ReleaseSemaphore(*h, 1, NULL);
|
||||
}
|
|
@ -17,11 +17,11 @@
|
|||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/fmt/fmt.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "third_party/nsync/array.internal.h"
|
||||
#include "third_party/nsync/cv.h"
|
||||
#include "third_party/nsync/heap.internal.h"
|
||||
#include "third_party/nsync/mu.h"
|
||||
#include "third_party/nsync/testing/array.h"
|
||||
#include "third_party/nsync/testing/closure.h"
|
||||
#include "third_party/nsync/testing/heap.h"
|
||||
#include "third_party/nsync/testing/smprintf.h"
|
||||
#include "third_party/nsync/testing/testing.h"
|
||||
#include "third_party/nsync/testing/time_extra.h"
|
||||
|
|
2
third_party/nsync/testing/dll_test.c
vendored
2
third_party/nsync/testing/dll_test.c
vendored
|
@ -18,8 +18,8 @@
|
|||
#include "libc/fmt/fmt.h"
|
||||
#include "libc/mem/mem.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "third_party/nsync/array.internal.h"
|
||||
#include "third_party/nsync/dll.h"
|
||||
#include "third_party/nsync/testing/array.h"
|
||||
#include "third_party/nsync/testing/smprintf.h"
|
||||
#include "third_party/nsync/testing/testing.h"
|
||||
// clang-format off
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/fmt/fmt.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "third_party/nsync/array.internal.h"
|
||||
#include "third_party/nsync/heap.internal.h"
|
||||
#include "third_party/nsync/mu.h"
|
||||
#include "third_party/nsync/mu_wait.h"
|
||||
#include "third_party/nsync/testing/array.h"
|
||||
#include "third_party/nsync/testing/closure.h"
|
||||
#include "third_party/nsync/testing/heap.h"
|
||||
#include "third_party/nsync/testing/smprintf.h"
|
||||
#include "third_party/nsync/testing/testing.h"
|
||||
#include "third_party/nsync/testing/time_extra.h"
|
||||
|
|
12
third_party/nsync/testing/testing.mk
vendored
12
third_party/nsync/testing/testing.mk
vendored
|
@ -12,8 +12,8 @@ THIRD_PARTY_NSYNC_TESTING_SRCS_TEST = $(filter %_test.c,$(THIRD_PARTY_NSYNC_TEST
|
|||
THIRD_PARTY_NSYNC_TESTING_OBJS = $(THIRD_PARTY_NSYNC_TESTING_SRCS:%.c=o/$(MODE)/%.o)
|
||||
THIRD_PARTY_NSYNC_TESTING_COMS = $(THIRD_PARTY_NSYNC_TESTING_SRCS_TEST:%.c=o/$(MODE)/%.com)
|
||||
THIRD_PARTY_NSYNC_TESTING_BINS = $(THIRD_PARTY_NSYNC_TESTING_COMS) $(THIRD_PARTY_NSYNC_TESTING_COMS:%=%.dbg)
|
||||
# THIRD_PARTY_NSYNC_TESTING_TESTS = $(THIRD_PARTY_NSYNC_TESTING_SRCS_TEST:%.c=o/$(MODE)/%.com.ok)
|
||||
# THIRD_PARTY_NSYNC_TESTING_CHECKS = $(THIRD_PARTY_NSYNC_TESTING_SRCS_TEST:%.c=o/$(MODE)/%.com.runs)
|
||||
THIRD_PARTY_NSYNC_TESTING_TESTS_ = $(THIRD_PARTY_NSYNC_TESTING_SRCS_TEST:%.c=o/$(MODE)/%.com.ok)
|
||||
THIRD_PARTY_NSYNC_TESTING_CHECKS_ = $(THIRD_PARTY_NSYNC_TESTING_SRCS_TEST:%.c=o/$(MODE)/%.com.runs)
|
||||
|
||||
THIRD_PARTY_NSYNC_TESTING_DIRECTDEPS = \
|
||||
LIBC_CALLS \
|
||||
|
@ -57,10 +57,10 @@ o/$(MODE)/third_party/nsync/testing/mu_test.com.runs: private QUOTA = -C64
|
|||
|
||||
.PHONY: o/$(MODE)/third_party/nsync/testing
|
||||
o/$(MODE)/third_party/nsync/testing: \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_CHECKS) \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_BINS)
|
||||
$(THIRD_PARTY_NSYNC_TESTING_CHECKS_) \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_BINS_)
|
||||
|
||||
.PHONY: o/$(MODE)/third_party/nsync/test
|
||||
o/$(MODE)/third_party/nsync/test: \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_CHECKS) \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_TESTS)
|
||||
$(THIRD_PARTY_NSYNC_TESTING_CHECKS_) \
|
||||
$(THIRD_PARTY_NSYNC_TESTING_TESTS_)
|
||||
|
|
14
third_party/nsync/testing/time_extra.c
vendored
14
third_party/nsync/testing/time_extra.c
vendored
|
@ -41,20 +41,6 @@ char *nsync_time_str (nsync_time t, int decimals) {
|
|||
return (smprintf ("%.*f%s", decimals, s/scale[i].multiplier, scale[i].suffix));
|
||||
}
|
||||
|
||||
int nsync_time_sleep_until (nsync_time abs_deadline) {
|
||||
int result = 0;
|
||||
nsync_time now;
|
||||
now = nsync_time_now ();
|
||||
if (nsync_time_cmp (abs_deadline, now) > 0) {
|
||||
nsync_time remaining;
|
||||
remaining = nsync_time_sleep (nsync_time_sub (abs_deadline, now));
|
||||
if (nsync_time_cmp (remaining, nsync_time_zero) > 0) {
|
||||
result = EINTR;
|
||||
}
|
||||
}
|
||||
return (result);
|
||||
}
|
||||
|
||||
double nsync_time_to_dbl (nsync_time t) {
|
||||
return (((double) NSYNC_TIME_SEC (t)) + ((double) NSYNC_TIME_NSEC (t) * 1e-9));
|
||||
}
|
||||
|
|
4
third_party/nsync/testing/time_extra.h
vendored
4
third_party/nsync/testing/time_extra.h
vendored
|
@ -6,10 +6,6 @@
|
|||
"decimals" decimal places. */
|
||||
char *nsync_time_str(nsync_time t, int decimals);
|
||||
|
||||
/* Sleep until the specified time. Returns 0 on success, and EINTR
|
||||
if the call was interrupted. */
|
||||
int nsync_time_sleep_until(nsync_time abs_deadline);
|
||||
|
||||
/* Return t as a double. */
|
||||
double nsync_time_to_dbl(nsync_time t);
|
||||
|
||||
|
|
2
third_party/nsync/testing/wait_test.c
vendored
2
third_party/nsync/testing/wait_test.c
vendored
|
@ -16,9 +16,9 @@
|
|||
│ limitations under the License. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/str/str.h"
|
||||
#include "third_party/nsync/array.internal.h"
|
||||
#include "third_party/nsync/counter.h"
|
||||
#include "third_party/nsync/note.h"
|
||||
#include "third_party/nsync/testing/array.h"
|
||||
#include "third_party/nsync/testing/closure.h"
|
||||
#include "third_party/nsync/testing/smprintf.h"
|
||||
#include "third_party/nsync/testing/testing.h"
|
||||
|
|
54
third_party/nsync/time.c
vendored
54
third_party/nsync/time.c
vendored
|
@ -1,54 +0,0 @@
|
|||
/*-*- mode:c;indent-tabs-mode:t;c-basic-offset:8;tab-width:8;coding:utf-8 -*-│
|
||||
│vi: set et ft=c ts=8 tw=8 fenc=utf-8 :vi│
|
||||
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||
│ Copyright 2016 Google Inc. │
|
||||
│ │
|
||||
│ Licensed under the Apache License, Version 2.0 (the "License"); │
|
||||
│ you may not use this file except in compliance with the License. │
|
||||
│ You may obtain a copy of the License at │
|
||||
│ │
|
||||
│ http://www.apache.org/licenses/LICENSE-2.0 │
|
||||
│ │
|
||||
│ Unless required by applicable law or agreed to in writing, software │
|
||||
│ distributed under the License is distributed on an "AS IS" BASIS, │
|
||||
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
|
||||
│ See the License for the specific language governing permissions and │
|
||||
│ limitations under the License. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/calls/struct/timespec.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "libc/sysv/consts/clock.h"
|
||||
#include "third_party/nsync/time.h"
|
||||
|
||||
asm(".ident\t\"\\n\\n\
|
||||
*NSYNC (Apache 2.0)\\n\
|
||||
Copyright 2016 Google, Inc.\\n\
|
||||
https://github.com/google/nsync\"");
|
||||
// clang-format off
|
||||
|
||||
#define NSYNC_NS_IN_S_ (1000 * 1000 * 1000)
|
||||
|
||||
/* Return the maximum t, assuming it's an integral
|
||||
type, and the representation is not too strange. */
|
||||
#define MAX_INT_TYPE(t) (((t)~(t)0) > 1? /*is t unsigned?*/ \
|
||||
(t)~(t)0 : /*unsigned*/ \
|
||||
(t) ((((uintmax_t)1) << (sizeof (t) * CHAR_BIT - 1)) - 1)) /*signed*/
|
||||
|
||||
const nsync_time nsync_time_no_deadline =
|
||||
NSYNC_TIME_STATIC_INIT (MAX_INT_TYPE (int64_t), NSYNC_NS_IN_S_ - 1);
|
||||
|
||||
const nsync_time nsync_time_zero = NSYNC_TIME_STATIC_INIT (0, 0);
|
||||
|
||||
nsync_time nsync_time_sleep (nsync_time delay) {
|
||||
struct timespec ts;
|
||||
struct timespec remain;
|
||||
memset (&ts, 0, sizeof (ts));
|
||||
ts.tv_sec = NSYNC_TIME_SEC (delay);
|
||||
ts.tv_nsec = NSYNC_TIME_NSEC (delay);
|
||||
if (nanosleep (&ts, &remain) == 0) {
|
||||
/* nanosleep() is not required to fill in "remain"
|
||||
if it returns 0. */
|
||||
memset (&remain, 0, sizeof (remain));
|
||||
}
|
||||
return (remain);
|
||||
}
|
13
third_party/nsync/time.h
vendored
13
third_party/nsync/time.h
vendored
|
@ -17,17 +17,21 @@ COSMOPOLITAN_C_START_
|
|||
typedef struct timespec nsync_time;
|
||||
|
||||
/* A deadline infinitely far in the future. */
|
||||
extern const nsync_time nsync_time_no_deadline;
|
||||
#define nsync_time_no_deadline _timespec_max
|
||||
|
||||
/* The zero delay, or an expired deadline. */
|
||||
extern const nsync_time nsync_time_zero;
|
||||
#define nsync_time_zero _timespec_zero
|
||||
|
||||
/* Return the current time since the epoch. */
|
||||
#define nsync_time_now() _timespec_real()
|
||||
|
||||
/* Sleep for the specified delay. Returns the unslept time which may be
|
||||
non-zero if the call was interrupted. */
|
||||
nsync_time nsync_time_sleep(nsync_time delay);
|
||||
#define nsync_time_sleep(a) _timespec_sleep(a)
|
||||
|
||||
/* Sleep until the specified time. Returns 0 on success, and EINTR
|
||||
if the call was interrupted. */
|
||||
#define nsync_time_sleep_until(a) _timespec_sleep_until(a)
|
||||
|
||||
/* Return a+b */
|
||||
#define nsync_time_add(a, b) _timespec_add(a, b)
|
||||
|
@ -44,6 +48,9 @@ nsync_time nsync_time_sleep(nsync_time delay);
|
|||
/* Return the specified number of microseconds as a time. */
|
||||
#define nsync_time_us(a) _timespec_frommicros(a)
|
||||
|
||||
/* Return the specified number of nanoseconds as a time. */
|
||||
#define nsync_time_ns(a) _timespec_fromnanos(a)
|
||||
|
||||
/* Return an nsync_time constructed from second and nanosecond
|
||||
components */
|
||||
#define nsync_time_s_ns(s, ns) ((nsync_time){(int64_t)(s), (unsigned)(ns)})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue