2001-05-23 15:26:42 +02:00
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// PROGRAM: Alice (All Else) Utility
|
|
|
|
// MODULE: all.cpp
|
|
|
|
// DESCRIPTION: Block allocator
|
|
|
|
//
|
|
|
|
// The contents of this file are subject to the Interbase Public
|
|
|
|
// License Version 1.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.Inprise.com/IPL.html
|
|
|
|
//
|
|
|
|
// Software distributed under the License is distributed on an
|
|
|
|
// "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
|
|
|
|
// or implied. See the License for the specific language governing
|
|
|
|
// rights and limitations under the License.
|
|
|
|
//
|
|
|
|
// The Original Code was created by Inprise Corporation
|
|
|
|
// and its predecessors. Portions created by Inprise Corporation are
|
|
|
|
// Copyright (C) Inprise Corporation.
|
|
|
|
//
|
|
|
|
// All Rights Reserved.
|
|
|
|
// Contributor(s): ______________________________________.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
2001-07-30 01:43:24 +02:00
|
|
|
// $Id: all.cpp,v 1.2 2001-07-29 23:43:20 skywalker Exp $
|
2001-05-23 15:26:42 +02:00
|
|
|
//
|
|
|
|
|
2001-07-30 01:43:24 +02:00
|
|
|
#include "firebird.h"
|
2001-05-23 15:26:42 +02:00
|
|
|
#include "../jrd/ib_stdio.h"
|
|
|
|
#include "../jrd/common.h"
|
|
|
|
#include "../jrd/ibsetjmp.h"
|
|
|
|
#include "../alice/alice.h"
|
|
|
|
#include "../alice/all.h"
|
|
|
|
#include "../alice/alloc.h"
|
|
|
|
#include "../alice/lls.h"
|
|
|
|
#include "../alice/all_proto.h"
|
|
|
|
#include "../jrd/gds_proto.h"
|
|
|
|
#include "../jrd/thd_proto.h"
|
|
|
|
|
|
|
|
#define BLKDEF(type, root, tail) sizeof (struct root), tail,
|
|
|
|
static struct {
|
|
|
|
SSHORT typ_root_length;
|
|
|
|
SSHORT typ_tail_length;
|
|
|
|
} block_sizes[] = {
|
|
|
|
0, 0,
|
|
|
|
#include "../alice/blk.h"
|
|
|
|
0};
|
|
|
|
|
|
|
|
#undef BLKDEF
|
|
|
|
|
|
|
|
static void extend_pool(PLB, SLONG);
|
|
|
|
static PLB find_pool(BLK);
|
|
|
|
static void release(FRB, PLB);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Allocate a block from a given pool and initialize the block.
|
|
|
|
// This is the primary block allocation routine.
|
|
|
|
//
|
|
|
|
|
|
|
|
BLK ALLA_alloc(PLB pool, UCHAR type, int count)
|
|
|
|
{
|
|
|
|
register BLK block;
|
|
|
|
FRB free, *best, *ptr;
|
|
|
|
SLONG best_tail, tail;
|
|
|
|
USHORT units;
|
|
|
|
register SLONG size;
|
|
|
|
register USHORT *p;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
if (type <= (UCHAR) type_MIN || type >= (SCHAR) type_MAX)
|
|
|
|
ALICE_error(62, 0, 0, 0, 0, 0); /* msg 62: bad block type */
|
|
|
|
|
|
|
|
// Compute block length
|
|
|
|
|
|
|
|
size = block_sizes[type].typ_root_length;
|
|
|
|
|
|
|
|
if ((tail = block_sizes[type].typ_tail_length) && count >= FUDGE)
|
|
|
|
size += (count - FUDGE) * tail;
|
|
|
|
|
|
|
|
size = (size + MIN_ALLOC - 1) & ~((ULONG) MIN_ALLOC - 1);
|
|
|
|
// TMN: Here we should probably have the following assert
|
|
|
|
// assert((size >> SHIFT) <= MAX_USHORT);
|
|
|
|
units = (USHORT) (size >> SHIFT);
|
|
|
|
|
|
|
|
if (size >= MAX_BLOCK)
|
|
|
|
ALICE_error(63, 0, 0, 0, 0, 0); /* msg 63:internal block exceeds maximum size */
|
|
|
|
|
|
|
|
// Find best fit. Best fit is defined to be the free block of shortest
|
|
|
|
// tail. If there isn't a fit, extend the pool and try, try again.
|
|
|
|
|
|
|
|
while (TRUE) {
|
|
|
|
best = NULL;
|
|
|
|
best_tail = MAX_BLOCK;
|
|
|
|
for (ptr = &pool->plb_free; (free = *ptr); ptr = &free->frb_next) {
|
|
|
|
if (free == free->frb_next)
|
|
|
|
ALICE_error(64, 0, 0, 0, 0, 0); /* msg 64: corrupt pool */
|
|
|
|
if ((tail = (int) free->frb_header.blk_length - (int) units) >= 0
|
|
|
|
&& tail < best_tail) {
|
|
|
|
best = ptr;
|
|
|
|
best_tail = tail;
|
|
|
|
if (tail == 0)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (best)
|
|
|
|
break;
|
|
|
|
extend_pool(pool, size);
|
|
|
|
}
|
|
|
|
|
|
|
|
// We've got our free block. If there's enough left of the free block
|
|
|
|
// after taking out our block, chop out out block. If not, allocate
|
|
|
|
// the entire free block as our block (a little extra won't hurt).
|
|
|
|
|
|
|
|
best_tail <<= SHIFT;
|
|
|
|
free = *best;
|
|
|
|
|
|
|
|
if (best_tail > sizeof(struct frb)) {
|
|
|
|
block =
|
|
|
|
(BLK) ((SCHAR *) free + (free->frb_header.blk_length << SHIFT));
|
|
|
|
block = (BLK) ((SCHAR *) block - size);
|
|
|
|
free->frb_header.blk_length -= units;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
*best = free->frb_next;
|
|
|
|
units = free->frb_header.blk_length;
|
|
|
|
size = units << SHIFT;
|
|
|
|
block = (BLK) free;
|
|
|
|
}
|
|
|
|
|
|
|
|
// zero out the allocated memory
|
|
|
|
|
|
|
|
size >>= 1;
|
|
|
|
p = (USHORT *) block;
|
|
|
|
do
|
|
|
|
*p++ = 0;
|
|
|
|
while (--size);
|
|
|
|
|
|
|
|
block->blk_type = type;
|
|
|
|
// TMN: Here we should probably have the following assert
|
|
|
|
// assert(pool->plb_pool_id <= MAX_UCHAR);
|
|
|
|
block->blk_pool_id = (UCHAR) pool->plb_pool_id;
|
|
|
|
block->blk_length = units;
|
|
|
|
|
|
|
|
return block;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Extend a repeating block, copying the constant part.
|
|
|
|
//
|
|
|
|
|
|
|
|
BLK ALLA_extend(BLK * pointer, int size)
|
|
|
|
{
|
|
|
|
BLK block;
|
|
|
|
PLB pool;
|
|
|
|
SLONG old_length, new_length;
|
|
|
|
register SCHAR *from, *to;
|
|
|
|
register SSHORT length;
|
|
|
|
|
|
|
|
block = *pointer;
|
|
|
|
pool = find_pool(block);
|
|
|
|
BLK new_ = ALLA_alloc(pool, block->blk_type, size);
|
|
|
|
|
|
|
|
old_length = block->blk_length << SHIFT;
|
|
|
|
new_length = new_->blk_length << SHIFT;
|
|
|
|
|
|
|
|
from = (SCHAR *) block + sizeof(struct blk);
|
|
|
|
to = (SCHAR *) new_ + sizeof(struct blk);
|
|
|
|
length = MIN(old_length, new_length) - sizeof(struct blk);
|
|
|
|
if (length)
|
|
|
|
do
|
|
|
|
*to++ = *from++;
|
|
|
|
while (--length);
|
|
|
|
|
|
|
|
release(reinterpret_cast < frb * >(block), pool);
|
|
|
|
|
|
|
|
if (new_->blk_type == (UCHAR) type_vec)
|
|
|
|
((VEC) new_)->vec_count = size;
|
|
|
|
else if (new_->blk_type == (UCHAR) type_vcl)
|
|
|
|
((VCL) new_)->vcl_count = size;
|
|
|
|
|
|
|
|
*pointer = new_;
|
|
|
|
|
|
|
|
return new_;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Get rid of everything.
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_fini(void)
|
|
|
|
{
|
|
|
|
register PLB pool, *vector, *until;
|
|
|
|
register HNK hunks, hunk;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
if (tdgbl->pools) {
|
|
|
|
for (vector =
|
|
|
|
(PLB *) tdgbl->pools->vec_object + tdgbl->pools->vec_count,
|
|
|
|
until = (PLB *) tdgbl->pools->vec_object; --vector >= until;)
|
|
|
|
if (pool = *vector)
|
|
|
|
for (hunks = pool->plb_hunks; hunk = hunks;) {
|
|
|
|
hunks = hunk->hnk_next;
|
|
|
|
ALLA_free(hunk->hnk_address);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tdgbl->pools = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Give space back to system.
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_free(SCHAR * memory)
|
|
|
|
{
|
|
|
|
|
|
|
|
gds__free(memory);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Initialize the pool system.
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_init(void)
|
|
|
|
{
|
|
|
|
SLONG temp_vector[20];
|
|
|
|
VEC vector;
|
|
|
|
PLB pool;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
tdgbl->pools = vector = (VEC) temp_vector;
|
|
|
|
vector->vec_count = 1;
|
|
|
|
vector->vec_object[0] = NULL;
|
|
|
|
|
|
|
|
tdgbl->ALICE_default_pool = tdgbl->ALICE_permanent_pool = pool =
|
|
|
|
ALLA_pool();
|
|
|
|
tdgbl->pools = vector = (VEC) ALLA_alloc(pool, type_vec, 10);
|
|
|
|
vector->vec_count = 10;
|
|
|
|
vector->vec_object[0] = (BLK) pool;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Get memory from system.
|
|
|
|
//
|
|
|
|
|
|
|
|
SCHAR *ALLA_malloc(SLONG size)
|
|
|
|
{
|
|
|
|
register SCHAR *memory;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
if (memory = (SCHAR *) gds__alloc(size))
|
|
|
|
return memory;
|
|
|
|
|
|
|
|
ALICE_error(65, 0, 0, 0, 0, 0); /* msg 65: virtual memory exhausted */
|
|
|
|
return (SCHAR *) 0; /* Keep compilers happy. ALICE_error() never returns. */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Allocate a new pool. This is done by creating a tempory
|
|
|
|
// pool block on the stack, then allocating a real pool block.
|
|
|
|
// In USHORT, by mirrors.
|
|
|
|
//
|
|
|
|
|
|
|
|
PLB ALLA_pool(void)
|
|
|
|
{
|
|
|
|
struct plb temp_pool;
|
|
|
|
register VEC vector;
|
|
|
|
register PLB pool;
|
|
|
|
register int pool_id;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
|
|
|
|
// Start by assigning a pool id
|
|
|
|
|
|
|
|
vector = tdgbl->pools;
|
|
|
|
|
|
|
|
for (pool_id = 0; pool_id < (int) vector->vec_count; pool_id++)
|
|
|
|
if (!(vector->vec_object[pool_id]))
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (pool_id >= (int) vector->vec_count)
|
|
|
|
vector =
|
|
|
|
(VEC) ALLA_extend(reinterpret_cast < blk ** >(&(tdgbl->pools)),
|
|
|
|
pool_id + 10);
|
|
|
|
|
|
|
|
vector->vec_object[pool_id] = (BLK) & temp_pool;
|
|
|
|
temp_pool.plb_free = NULL;
|
|
|
|
temp_pool.plb_hunks = NULL;
|
|
|
|
temp_pool.plb_pool_id = pool_id;
|
|
|
|
if (pool_id == 0)
|
|
|
|
tdgbl->ALICE_permanent_pool = &temp_pool;
|
|
|
|
|
|
|
|
pool = (PLB) ALLA_alloc(&temp_pool, type_plb, 0);
|
|
|
|
pool->plb_pool_id = pool_id;
|
|
|
|
pool->plb_free = temp_pool.plb_free;
|
|
|
|
pool->plb_hunks = temp_pool.plb_hunks;
|
|
|
|
vector->vec_object[pool_id] = (BLK) pool;
|
|
|
|
|
|
|
|
if (pool_id == 0)
|
|
|
|
tdgbl->ALICE_permanent_pool = pool;
|
|
|
|
|
|
|
|
return pool;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Push an object on an LLS stack.
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_push(BLK object, register LLS * stack)
|
|
|
|
{
|
|
|
|
register LLS node;
|
|
|
|
PLB pool;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
pool = tdgbl->ALICE_default_pool;
|
|
|
|
|
|
|
|
if (node = pool->plb_lls)
|
|
|
|
pool->plb_lls = node->lls_next;
|
|
|
|
else
|
|
|
|
node = (LLS) ALLA_alloc(pool, type_lls, 0);
|
|
|
|
|
|
|
|
node->lls_object = object;
|
|
|
|
node->lls_next = *stack;
|
|
|
|
*stack = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Pop an object off a linked list stack. Save the node for
|
|
|
|
// further use.
|
|
|
|
//
|
|
|
|
|
|
|
|
BLK ALLA_pop(register LLS * stack)
|
|
|
|
{
|
|
|
|
register LLS node;
|
|
|
|
register PLB pool;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
|
|
|
|
node = *stack;
|
|
|
|
pool = (PLB) tdgbl->pools->vec_object[node->lls_header.blk_pool_id];
|
|
|
|
*stack = node->lls_next;
|
|
|
|
node->lls_next = pool->plb_lls;
|
|
|
|
pool->plb_lls = node;
|
|
|
|
|
|
|
|
return node->lls_object;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Release a block to its pool. If it is contiguous to
|
|
|
|
// another free block, combine them. Otherwise link it
|
|
|
|
// into the free block linked list (kept in ascending order
|
|
|
|
// of addresses).
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_release(register FRB block)
|
|
|
|
{
|
|
|
|
|
|
|
|
release(block, find_pool(block ? &block->frb_header : (BLK) 0));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Release a storage pool. This involves nothing more than returning
|
|
|
|
// hunks to the free hunk list.
|
|
|
|
//
|
|
|
|
|
|
|
|
void ALLA_rlpool(PLB pool)
|
|
|
|
{
|
|
|
|
register HNK hunk, hunks;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
|
|
|
|
tdgbl->pools->vec_object[pool->plb_pool_id] = NULL;
|
|
|
|
|
|
|
|
for (hunks = pool->plb_hunks; hunk = hunks;) {
|
|
|
|
hunks = hunk->hnk_next;
|
|
|
|
ALLA_free(hunk->hnk_address);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Allocate, extend, or no-op a vector making sure it is "long enough".
|
|
|
|
//
|
|
|
|
|
|
|
|
VEC ALLA_vector(PLB pool, VEC * ptr, USHORT count)
|
|
|
|
{
|
|
|
|
VEC vector;
|
|
|
|
|
|
|
|
++count;
|
|
|
|
|
|
|
|
if (!(vector = *ptr)) {
|
|
|
|
vector = *ptr = (VEC) ALLA_alloc(pool, type_vec, count);
|
|
|
|
vector->vec_count = count;
|
|
|
|
return vector;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (vector->vec_count >= count)
|
|
|
|
return vector;
|
|
|
|
|
|
|
|
return (VEC) ALLA_extend(reinterpret_cast < blk ** >(ptr), count);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Extend a pool by at least enough to accomodate a block
|
|
|
|
// of given size.
|
|
|
|
//
|
|
|
|
|
|
|
|
static void extend_pool(PLB pool, SLONG size)
|
|
|
|
{
|
|
|
|
register HNK hunk;
|
|
|
|
register BLK block;
|
|
|
|
|
|
|
|
size =
|
|
|
|
(size + sizeof(struct hnk) + MIN_ALLOCATION -
|
|
|
|
1) & ~((ULONG) MIN_ALLOCATION - 1);
|
|
|
|
block = (BLK) ALLA_malloc((SLONG) size);
|
|
|
|
// TMN: Here we should probably have the following assert
|
|
|
|
// assert((size >> SHIFT) <= MAX_USHORT);
|
|
|
|
block->blk_length = (USHORT) size >> SHIFT;
|
|
|
|
block->blk_type = (UCHAR) type_frb;
|
|
|
|
// TMN: Here we should probably have the following assert
|
|
|
|
// assert(pool->plb_pool_id <= MAX_UCHAR);
|
|
|
|
block->blk_pool_id = (UCHAR) pool->plb_pool_id;
|
|
|
|
release(reinterpret_cast < frb * >(block), pool);
|
|
|
|
|
|
|
|
hunk = (HNK) ALLA_alloc(pool, type_hnk, 0);
|
|
|
|
hunk->hnk_address = (SCHAR *) block;
|
|
|
|
hunk->hnk_length = size;
|
|
|
|
hunk->hnk_next = pool->plb_hunks;
|
|
|
|
pool->plb_hunks = hunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Find pool associate with block.
|
|
|
|
//
|
|
|
|
|
|
|
|
static PLB find_pool(BLK block)
|
|
|
|
{
|
|
|
|
PLB pool;
|
|
|
|
HNK hunk;
|
|
|
|
USHORT pool_id;
|
|
|
|
TGBL tdgbl;
|
|
|
|
|
|
|
|
tdgbl = GET_THREAD_DATA;
|
|
|
|
|
|
|
|
|
|
|
|
for (pool_id = block->blk_pool_id;
|
|
|
|
pool_id < tdgbl->pools->vec_count
|
|
|
|
&& (pool = (PLB) tdgbl->pools->vec_object[pool_id]); pool_id += 256)
|
|
|
|
for (hunk = pool->plb_hunks; hunk; hunk = hunk->hnk_next)
|
|
|
|
if ((SCHAR *) block >= hunk->hnk_address
|
|
|
|
&& (SCHAR *) block < hunk->hnk_address + hunk->hnk_length)
|
|
|
|
return pool;
|
|
|
|
|
|
|
|
ALICE_error(66, 0, 0, 0, 0, 0); /* msg 66: bad pool id */
|
|
|
|
return (PLB) 0; /* Keep compilers happy. ALICE_error() never returns. */
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//____________________________________________________________
|
|
|
|
//
|
|
|
|
// Release a block to its pool. If it is contiguous to
|
|
|
|
// another free block, combine them. Otherwise link it
|
|
|
|
// into the free block linked list (kept in ascending order
|
|
|
|
// of addresses).
|
|
|
|
//
|
|
|
|
|
|
|
|
static void release(FRB block, PLB pool)
|
|
|
|
{
|
|
|
|
register FRB prior, free;
|
|
|
|
FRB *ptr;
|
|
|
|
SLONG length;
|
|
|
|
|
|
|
|
block->frb_header.blk_type = (UCHAR) type_frb;
|
|
|
|
prior = NULL;
|
|
|
|
|
|
|
|
for (ptr = &pool->plb_free; free = *ptr;
|
|
|
|
prior = free, ptr = &free->frb_next) if (block < free)
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Merge block into list first, then try to combine blocks
|
|
|
|
|
|
|
|
block->frb_next = free;
|
|
|
|
*ptr = block;
|
|
|
|
|
|
|
|
// Try to merge the free block with the next one down.
|
|
|
|
|
|
|
|
length = block->frb_header.blk_length << SHIFT;
|
|
|
|
|
|
|
|
if (free && (SCHAR *) block + length == (SCHAR *) free) {
|
|
|
|
block->frb_header.blk_length += free->frb_header.blk_length;
|
|
|
|
block->frb_next = free->frb_next;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try and merge the block with the prior free block
|
|
|
|
|
|
|
|
if (prior &&
|
|
|
|
(length = prior->frb_header.blk_length << SHIFT) &&
|
|
|
|
(SCHAR *) prior + length == (SCHAR *) block) {
|
|
|
|
prior->frb_header.blk_length += block->frb_header.blk_length;
|
|
|
|
prior->frb_next = block->frb_next;
|
|
|
|
}
|
|
|
|
}
|