Commit 1ebb5fcc by Richard Kenner

(sarray_{remove,free}_garbage): New functions.

(sarray_at_put, sarray_new, sarray_lazy_copy):
Modify/copy sarray structure/data in a thread-safe manner
(sarray_{realloc,free}): Reallocate/free sarray structure/data in a
thread-safe manner.

From-SVN: r11445
parent 54d643f6
/* Sparse Arrays for Objective C dispatch tables /* Sparse Arrays for Objective C dispatch tables
Copyright (C) 1993, 1995 Free Software Foundation, Inc. Copyright (C) 1993, 1995, 1996 Free Software Foundation, Inc.
This file is part of GNU CC. This file is part of GNU CC.
...@@ -25,13 +25,16 @@ Boston, MA 02111-1307, USA. */ ...@@ -25,13 +25,16 @@ Boston, MA 02111-1307, USA. */
the executable file might be covered by the GNU General Public License. */ the executable file might be covered by the GNU General Public License. */
#include "objc/sarray.h" #include "objc/sarray.h"
#include "objc/runtime.h"
#include <stdio.h> #include <stdio.h>
#include "assert.h" #include "assert.h"
int nbuckets = 0; int nbuckets = 0; /* !T:MUTEX */
int nindices = 0; int nindices = 0; /* !T:MUTEX */
int narrays = 0; int narrays = 0; /* !T:MUTEX */
int idxsize = 0; int idxsize = 0; /* !T:MUTEX */
static void * first_free_data = NULL; /* !T:MUTEX */
#ifdef OBJC_SPARSE2 #ifdef OBJC_SPARSE2
const char* __objc_sparse2_id = "2 level sparse indices"; const char* __objc_sparse2_id = "2 level sparse indices";
...@@ -46,13 +49,60 @@ const void *memcpy (void*, const void*, size_t); ...@@ -46,13 +49,60 @@ const void *memcpy (void*, const void*, size_t);
void free (const void*); void free (const void*);
#endif #endif
/* This function removes any structures left over from free operations
that were not safe in a multi-threaded environment. */
void
sarray_remove_garbage(void)
{
void **vp;
void *np;
objc_mutex_lock(__objc_runtime_mutex);
vp = first_free_data;
first_free_data = NULL;
while (vp) {
np = *vp;
free(vp);
vp = np;
}
objc_mutex_unlock(__objc_runtime_mutex);
}
/* Free a block of dynamically allocated memory. If we are in multi-threaded
mode, it is ok to free it. If not, we add it to the garbage heap to be
freed later. */
static void
sarray_free_garbage(void *vp)
{
objc_mutex_lock(__objc_runtime_mutex);
if (__objc_runtime_threads_alive == 1) {
free(vp);
if (first_free_data)
sarray_remove_garbage();
}
else {
*(void **)vp = first_free_data;
first_free_data = vp;
}
objc_mutex_unlock(__objc_runtime_mutex);
}
/* sarray_at_put : copies data in such a way as to be thread reader safe. */
void void
sarray_at_put(struct sarray* array, sidx index, void* element) sarray_at_put(struct sarray* array, sidx index, void* element)
{ {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
struct sindex** the_index; struct sindex** the_index;
struct sindex* new_index;
#endif #endif
struct sbucket** the_bucket; struct sbucket** the_bucket;
struct sbucket* new_bucket;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
size_t ioffset; size_t ioffset;
#endif #endif
...@@ -96,22 +146,24 @@ sarray_at_put(struct sarray* array, sidx index, void* element) ...@@ -96,22 +146,24 @@ sarray_at_put(struct sarray* array, sidx index, void* element)
if ((*the_index) == array->empty_index) { if ((*the_index) == array->empty_index) {
/* The index was previously empty, allocate a new */ /* The index was previously empty, allocate a new */
*the_index = (struct sindex*)__objc_xmalloc(sizeof(struct sindex)); new_index = (struct sindex*)__objc_xmalloc(sizeof(struct sindex));
memcpy(*the_index, array->empty_index, sizeof(struct sindex)); memcpy(new_index, array->empty_index, sizeof(struct sindex));
(*the_index)->version = array->version; new_index->version.version = array->version.version;
*the_index = new_index; /* Prepared for install. */
the_bucket = &((*the_index)->buckets[boffset]); the_bucket = &((*the_index)->buckets[boffset]);
nindices += 1;
} else if ((*the_index)->version != array->version) { nindices += 1;
} else if ((*the_index)->version.version != array->version.version) {
/* This index must be lazy copied */ /* This index must be lazy copied */
struct sindex* old_index = *the_index; struct sindex* old_index = *the_index;
*the_index = (struct sindex*)__objc_xmalloc(sizeof(struct sindex)); new_index = (struct sindex*)__objc_xmalloc(sizeof(struct sindex));
memcpy( *the_index,old_index, sizeof(struct sindex)); memcpy( new_index, old_index, sizeof(struct sindex));
(*the_index)->version = array->version; new_index->version.version = array->version.version;
*the_index = new_index; /* Prepared for install. */
the_bucket = &((*the_index)->buckets[boffset]); the_bucket = &((*the_index)->buckets[boffset]);
nindices += 1;
nindices += 1;
} }
#endif /* OBJC_SPARSE3 */ #endif /* OBJC_SPARSE3 */
...@@ -122,18 +174,22 @@ sarray_at_put(struct sarray* array, sidx index, void* element) ...@@ -122,18 +174,22 @@ sarray_at_put(struct sarray* array, sidx index, void* element)
/* The bucket was previously empty (or something like that), */ /* The bucket was previously empty (or something like that), */
/* allocate a new. This is the effect of `lazy' allocation */ /* allocate a new. This is the effect of `lazy' allocation */
*the_bucket = (struct sbucket*)__objc_xmalloc(sizeof(struct sbucket)); new_bucket = (struct sbucket*)__objc_xmalloc(sizeof(struct sbucket));
memcpy((void *) *the_bucket, (const void*)array->empty_bucket, sizeof(struct sbucket)); memcpy((void *) new_bucket, (const void*)array->empty_bucket, sizeof(struct sbucket));
(*the_bucket)->version = array->version; new_bucket->version.version = array->version.version;
*the_bucket = new_bucket; /* Prepared for install. */
nbuckets += 1; nbuckets += 1;
} else if ((*the_bucket)->version != array->version) { } else if ((*the_bucket)->version.version != array->version.version) {
/* Perform lazy copy. */ /* Perform lazy copy. */
struct sbucket* old_bucket = *the_bucket; struct sbucket* old_bucket = *the_bucket;
*the_bucket = (struct sbucket*)__objc_xmalloc(sizeof(struct sbucket)); new_bucket = (struct sbucket*)__objc_xmalloc(sizeof(struct sbucket));
memcpy( *the_bucket,old_bucket, sizeof(struct sbucket)); memcpy( new_bucket, old_bucket, sizeof(struct sbucket));
(*the_bucket)->version = array->version; new_bucket->version.version = array->version.version;
*the_bucket = new_bucket; /* Prepared for install. */
nbuckets += 1; nbuckets += 1;
} }
...@@ -151,42 +207,48 @@ sarray_at_put_safe(struct sarray* array, sidx index, void* element) ...@@ -151,42 +207,48 @@ sarray_at_put_safe(struct sarray* array, sidx index, void* element)
struct sarray* struct sarray*
sarray_new (int size, void* default_element) sarray_new (int size, void* default_element)
{ {
struct sarray* arr;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
size_t num_indices = ((size-1)/(INDEX_CAPACITY))+1; size_t num_indices = ((size-1)/(INDEX_CAPACITY))+1;
struct sindex ** new_indices;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
size_t num_indices = ((size-1)/BUCKET_SIZE)+1; size_t num_indices = ((size-1)/BUCKET_SIZE)+1;
struct sbucket ** new_buckets;
#endif #endif
int counter; int counter;
struct sarray* arr;
assert(size > 0); assert(size > 0);
/* Allocate core array */ /* Allocate core array */
arr = (struct sarray*) __objc_xmalloc(sizeof(struct sarray)); arr = (struct sarray*) __objc_xmalloc(sizeof(struct sarray));
arr->version = 0; arr->version.version = 0;
narrays += 1;
/* Initialize members */ /* Initialize members */
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
arr->capacity = num_indices*INDEX_CAPACITY; arr->capacity = num_indices*INDEX_CAPACITY;
arr->indices = (struct sindex**) new_indices = (struct sindex**)
__objc_xmalloc(sizeof(struct sindex*)*num_indices); __objc_xmalloc(sizeof(struct sindex*)*num_indices);
idxsize += num_indices;
arr->empty_index = (struct sindex*) __objc_xmalloc(sizeof(struct sindex)); arr->empty_index = (struct sindex*) __objc_xmalloc(sizeof(struct sindex));
arr->empty_index->version = 0; arr->empty_index->version.version = 0;
narrays += 1;
idxsize += num_indices;
nindices += 1; nindices += 1;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
arr->capacity = num_indices*BUCKET_SIZE; arr->capacity = num_indices*BUCKET_SIZE;
arr->buckets = (struct sbucket**) new_buckets = (struct sbucket**)
__objc_xmalloc(sizeof(struct sbucket*)*num_indices); __objc_xmalloc(sizeof(struct sbucket*)*num_indices);
narrays += 1;
idxsize += num_indices; idxsize += num_indices;
#endif #endif
arr->empty_bucket = (struct sbucket*) __objc_xmalloc(sizeof(struct sbucket)); arr->empty_bucket = (struct sbucket*) __objc_xmalloc(sizeof(struct sbucket));
arr->empty_bucket->version = 0; arr->empty_bucket->version.version = 0;
nbuckets += 1; nbuckets += 1;
arr->ref_count = 1; arr->ref_count = 1;
...@@ -200,20 +262,28 @@ sarray_new (int size, void* default_element) ...@@ -200,20 +262,28 @@ sarray_new (int size, void* default_element)
arr->empty_index->buckets[counter] = arr->empty_bucket; arr->empty_index->buckets[counter] = arr->empty_bucket;
for (counter=0; counter<num_indices; counter++) for (counter=0; counter<num_indices; counter++)
arr->indices[counter] = arr->empty_index; new_indices[counter] = arr->empty_index;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
for (counter=0; counter<num_indices; counter++) for (counter=0; counter<num_indices; counter++)
arr->buckets[counter] = arr->empty_bucket; new_buckets[counter] = arr->empty_bucket;
#endif
#ifdef OBJC_SPARSE3
arr->indices = new_indices;
#else /* OBJC_SPARSE2 */
arr->buckets = new_buckets;
#endif #endif
return arr; return arr;
} }
/* Reallocate the sparse array to hold `newsize' entries */ /* Reallocate the sparse array to hold `newsize' entries
Note: We really allocate and then free. We have to do this to ensure that
any concurrent readers notice the update. */
void void
sarray_realloc(struct sarray* array, int newsize) sarray_realloc(struct sarray* array, int newsize)
...@@ -223,11 +293,17 @@ sarray_realloc(struct sarray* array, int newsize) ...@@ -223,11 +293,17 @@ sarray_realloc(struct sarray* array, int newsize)
size_t new_max_index = ((newsize-1)/INDEX_CAPACITY); size_t new_max_index = ((newsize-1)/INDEX_CAPACITY);
size_t rounded_size = (new_max_index+1)*INDEX_CAPACITY; size_t rounded_size = (new_max_index+1)*INDEX_CAPACITY;
struct sindex ** new_indices;
struct sindex ** old_indices;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
size_t old_max_index = (array->capacity-1)/BUCKET_SIZE; size_t old_max_index = (array->capacity-1)/BUCKET_SIZE;
size_t new_max_index = ((newsize-1)/BUCKET_SIZE); size_t new_max_index = ((newsize-1)/BUCKET_SIZE);
size_t rounded_size = (new_max_index+1)*BUCKET_SIZE; size_t rounded_size = (new_max_index+1)*BUCKET_SIZE;
struct sbucket ** new_buckets;
struct sbucket ** old_buckets;
#endif #endif
int counter; int counter;
...@@ -235,87 +311,72 @@ sarray_realloc(struct sarray* array, int newsize) ...@@ -235,87 +311,72 @@ sarray_realloc(struct sarray* array, int newsize)
assert(newsize > 0); assert(newsize > 0);
/* The size is the same, just ignore the request */ /* The size is the same, just ignore the request */
if(rounded_size == array->capacity) if(rounded_size <= array->capacity)
return; return;
assert(array->ref_count == 1); /* stop if lazy copied... */ assert(array->ref_count == 1); /* stop if lazy copied... */
if(rounded_size < array->capacity) /* We are asked to extend the array -- allocate new bucket table, */
/* and insert empty_bucket in newly allocated places. */
if(rounded_size > array->capacity)
{ {
#ifdef OBJC_SPARSE3
new_max_index += 4;
rounded_size = (new_max_index+1)*INDEX_CAPACITY;
#else /* OBJC_SPARSE2 */
new_max_index += 4;
rounded_size = (new_max_index+1)*BUCKET_SIZE;
#endif
/* update capacity */ /* update capacity */
array->capacity = rounded_size; array->capacity = rounded_size;
/* free buckets above new_max_index */
for(counter = old_max_index; counter > new_max_index; counter-- ) {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
struct sindex* idx = array->indices[counter]; /* alloc to force re-read by any concurrent readers. */
if((idx != array->empty_index) && (idx->version == array->version)) { old_indices = array->indices;
int c2; new_indices = (struct sindex**)
for(c2=0; c2<INDEX_SIZE; c2++) { __objc_xmalloc((new_max_index+1)*sizeof(struct sindex*));
struct sbucket* bkt = idx->buckets[c2];
if((bkt != array->empty_bucket) && (bkt->version == array->version))
{
free(bkt);
nbuckets -= 1;
}
}
free(idx);
nindices -= 1;
}
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
struct sbucket* bkt = array->buckets[counter]; old_buckets = array->buckets;
if ((bkt != array->empty_bucket) && (bkt->version == array->version)) new_buckets = (struct sbucket**)
{ __objc_xmalloc((new_max_index+1)*sizeof(struct sbucket*));
free(bkt);
nbuckets -= 1;
}
#endif #endif
}
/* copy buckets below old_max_index (they are still valid) */
for(counter = 0; counter <= old_max_index; counter++ ) {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
/* realloc to free the space above new_max_index */ new_indices[counter] = old_indices[counter];
array->indices = (struct sindex**)
__objc_xrealloc(array->indices,
(new_max_index+1)*sizeof(struct sindex*));
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
array->buckets = (struct sbucket**) new_buckets[counter] = old_buckets[counter];
__objc_xrealloc(array->buckets,
(new_max_index+1)*sizeof(struct sbucket*));
#endif #endif
idxsize -= (old_max_index-new_max_index);
return;
} }
/* We are asked to extend the array -- reallocate the bucket table, */
/* and insert empty_bucket in newly allocated places. */
if(rounded_size > array->capacity)
{
/* update capacity */
array->capacity = rounded_size;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
/* realloc to make room in table above old_max_index */
array->indices = (struct sindex**)
__objc_xrealloc(array->indices,
(new_max_index+1)*sizeof(struct sindex*));
/* reset entries above old_max_index to empty_bucket */ /* reset entries above old_max_index to empty_bucket */
for(counter = old_max_index+1; counter <= new_max_index; counter++) for(counter = old_max_index+1; counter <= new_max_index; counter++)
array->indices[counter] = array->empty_index; new_indices[counter] = array->empty_index;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
/* realloc to make room in table above old_max_index */
array->buckets = (struct sbucket**)
__objc_xrealloc(array->buckets,
(new_max_index+1)*sizeof(struct sbucket*));
/* reset entries above old_max_index to empty_bucket */ /* reset entries above old_max_index to empty_bucket */
for(counter = old_max_index+1; counter <= new_max_index; counter++) for(counter = old_max_index+1; counter <= new_max_index; counter++)
array->buckets[counter] = array->empty_bucket; new_buckets[counter] = array->empty_bucket;
#endif
#ifdef OBJC_SPARSE3
/* install the new indices */
array->indices = new_indices;
#else /* OBJC_SPARSE2 */
array->buckets = new_buckets;
#endif #endif
#ifdef OBJC_SPARSE3
/* free the old indices */
sarray_free_garbage(old_indices);
#else /* OBJC_SPARSE2 */
sarray_free_garbage(old_buckets);
#endif
idxsize += (new_max_index-old_max_index); idxsize += (new_max_index-old_max_index);
return; return;
} }
...@@ -326,10 +387,13 @@ sarray_realloc(struct sarray* array, int newsize) ...@@ -326,10 +387,13 @@ sarray_realloc(struct sarray* array, int newsize)
void void
sarray_free(struct sarray* array) { sarray_free(struct sarray* array) {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
size_t old_max_index = (array->capacity-1)/INDEX_CAPACITY; size_t old_max_index = (array->capacity-1)/INDEX_CAPACITY;
struct sindex ** old_indices;
#else #else
size_t old_max_index = (array->capacity-1)/BUCKET_SIZE; size_t old_max_index = (array->capacity-1)/BUCKET_SIZE;
struct sbucket ** old_buckets;
#endif #endif
int counter = 0; int counter = 0;
...@@ -338,31 +402,40 @@ sarray_free(struct sarray* array) { ...@@ -338,31 +402,40 @@ sarray_free(struct sarray* array) {
if(--(array->ref_count) != 0) /* There exists copies of me */ if(--(array->ref_count) != 0) /* There exists copies of me */
return; return;
#ifdef OBJC_SPARSE3
old_indices = array->indices;
#else
old_buckets = array->buckets;
#endif
if((array->is_copy_of) && ((array->is_copy_of->ref_count - 1) == 0)) if((array->is_copy_of) && ((array->is_copy_of->ref_count - 1) == 0))
sarray_free(array->is_copy_of); sarray_free(array->is_copy_of);
/* Free all entries that do not point to empty_bucket */ /* Free all entries that do not point to empty_bucket */
for(counter = 0; counter <= old_max_index; counter++ ) { for(counter = 0; counter <= old_max_index; counter++ ) {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
struct sindex* idx = array->indices[counter]; struct sindex* idx = old_indices[counter];
if((idx != array->empty_index) && (idx->version == array->version)) { if((idx != array->empty_index) &&
(idx->version.version == array->version.version)) {
int c2; int c2;
for(c2=0; c2<INDEX_SIZE; c2++) { for(c2=0; c2<INDEX_SIZE; c2++) {
struct sbucket* bkt = idx->buckets[c2]; struct sbucket* bkt = idx->buckets[c2];
if((bkt != array->empty_bucket) && (bkt->version == array->version)) if((bkt != array->empty_bucket) &&
(bkt->version.version == array->version.version))
{ {
free(bkt); sarray_free_garbage(bkt);
nbuckets -= 1; nbuckets -= 1;
} }
} }
free(idx); sarray_free_garbage(idx);
nindices -= 1; nindices -= 1;
} }
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
struct sbucket* bkt = array->buckets[counter]; struct sbucket* bkt = array->buckets[counter];
if ((bkt != array->empty_bucket) && (bkt->version == array->version)) if ((bkt != array->empty_bucket) &&
(bkt->version.version == array->version.version))
{ {
free(bkt); sarray_free_garbage(bkt);
nbuckets -= 1; nbuckets -= 1;
} }
#endif #endif
...@@ -370,33 +443,32 @@ sarray_free(struct sarray* array) { ...@@ -370,33 +443,32 @@ sarray_free(struct sarray* array) {
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
/* free empty_index */ /* free empty_index */
if(array->empty_index->version == array->version) { if(array->empty_index->version.version == array->version.version) {
free(array->empty_index); sarray_free_garbage(array->empty_index);
nindices -= 1; nindices -= 1;
} }
#endif #endif
/* free empty_bucket */ /* free empty_bucket */
if(array->empty_bucket->version == array->version) { if(array->empty_bucket->version.version == array->version.version) {
free(array->empty_bucket); sarray_free_garbage(array->empty_bucket);
nbuckets -= 1; nbuckets -= 1;
} }
idxsize -= (old_max_index+1);
narrays -= 1;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
/* free bucket table */ /* free bucket table */
free(array->indices); sarray_free_garbage(array->indices);
idxsize -= (old_max_index+1);
#else #else
/* free bucket table */ /* free bucket table */
free(array->buckets); sarray_free_garbage(array->buckets);
idxsize -= (old_max_index+1);
#endif #endif
/* free array */ /* free array */
free(array); sarray_free_garbage(array);
narrays -= 1;
} }
/* This is a lazy copy. Only the core of the structure is actually */ /* This is a lazy copy. Only the core of the structure is actually */
...@@ -405,33 +477,42 @@ sarray_free(struct sarray* array) { ...@@ -405,33 +477,42 @@ sarray_free(struct sarray* array) {
struct sarray* struct sarray*
sarray_lazy_copy(struct sarray* oarr) sarray_lazy_copy(struct sarray* oarr)
{ {
struct sarray* arr;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
size_t num_indices = ((oarr->capacity-1)/INDEX_CAPACITY)+1; size_t num_indices = ((oarr->capacity-1)/INDEX_CAPACITY)+1;
struct sindex ** new_indices;
#else /* OBJC_SPARSE2 */ #else /* OBJC_SPARSE2 */
size_t num_indices = ((oarr->capacity-1)/BUCKET_SIZE)+1; size_t num_indices = ((oarr->capacity-1)/BUCKET_SIZE)+1;
struct sbucket ** new_buckets;
#endif #endif
struct sarray* arr;
/* Allocate core array */ /* Allocate core array */
arr = (struct sarray*) __objc_xmalloc(sizeof(struct sarray)); arr = (struct sarray*) __objc_xmalloc(sizeof(struct sarray)); /* !!! */
memcpy( arr,oarr, sizeof(struct sarray)); arr->version.version = oarr->version.version + 1;
arr->version = oarr->version + 1; #ifdef OBJC_SPARSE3
arr->is_copy_of = oarr; arr->empty_index = oarr->empty_index;
oarr->ref_count += 1; #endif
arr->empty_bucket = oarr->empty_bucket;
arr->ref_count = 1; arr->ref_count = 1;
oarr->ref_count += 1;
arr->is_copy_of = oarr;
arr->capacity = oarr->capacity;
#ifdef OBJC_SPARSE3 #ifdef OBJC_SPARSE3
/* Copy bucket table */ /* Copy bucket table */
arr->indices = (struct sindex**) new_indices = (struct sindex**)
__objc_xmalloc(sizeof(struct sindex*)*num_indices); __objc_xmalloc(sizeof(struct sindex*)*num_indices);
memcpy( arr->indices,oarr->indices, memcpy( new_indices,oarr->indices,
sizeof(struct sindex*)*num_indices); sizeof(struct sindex*)*num_indices);
arr->indices = new_indices;
#else #else
/* Copy bucket table */ /* Copy bucket table */
arr->buckets = (struct sbucket**) new_buckets = (struct sbucket**)
__objc_xmalloc(sizeof(struct sbucket*)*num_indices); __objc_xmalloc(sizeof(struct sbucket*)*num_indices);
memcpy( arr->buckets,oarr->buckets, memcpy( new_buckets,oarr->buckets,
sizeof(struct sbucket*)*num_indices); sizeof(struct sbucket*)*num_indices);
arr->buckets = new_buckets;
#endif #endif
idxsize += num_indices; idxsize += num_indices;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment