2008-11-24 04:21:23 +00:00
|
|
|
/*
|
2014-01-19 01:09:44 +00:00
|
|
|
* This file Copyright (C) 2008-2014 Mnemosyne LLC
|
2008-11-24 04:21:23 +00:00
|
|
|
*
|
2014-01-21 03:10:30 +00:00
|
|
|
* It may be used under the GNU GPL versions 2 or 3
|
2014-01-19 01:09:44 +00:00
|
|
|
* or any future license endorsed by Mnemosyne LLC.
|
2008-11-24 04:21:23 +00:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2021-09-19 20:41:35 +00:00
|
|
|
#include <algorithm>
|
|
|
|
#include <cstring> /* memset() */
|
2021-11-09 03:30:03 +00:00
|
|
|
#include <vector>
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2008-11-24 04:21:23 +00:00
|
|
|
#include "transmission.h"
|
|
|
|
#include "bandwidth.h"
|
2017-04-21 07:40:57 +00:00
|
|
|
#include "crypto-utils.h" /* tr_rand_int_weak() */
|
2013-01-25 23:34:20 +00:00
|
|
|
#include "log.h"
|
2008-12-16 22:08:17 +00:00
|
|
|
#include "peer-io.h"
|
2017-06-08 07:24:12 +00:00
|
|
|
#include "tr-assert.h"
|
2008-11-24 04:21:23 +00:00
|
|
|
#include "utils.h"
|
|
|
|
|
2021-09-15 00:18:09 +00:00
|
|
|
#define dbgmsg(...) tr_logAddDeepNamed(nullptr, __VA_ARGS__)
|
2009-01-02 21:50:51 +00:00
|
|
|
|
2008-11-24 04:21:23 +00:00
|
|
|
/***
|
|
|
|
****
|
|
|
|
***/
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
unsigned int Bandwidth::getSpeedBytesPerSecond(RateControl& r, unsigned int interval_msec, uint64_t now)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2017-04-30 16:25:26 +00:00
|
|
|
if (now == 0)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
now = tr_time_msec();
|
|
|
|
}
|
2008-11-24 04:21:23 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (now != r.cache_time_)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
uint64_t bytes = 0;
|
2017-04-20 16:02:19 +00:00
|
|
|
uint64_t const cutoff = now - interval_msec;
|
2008-11-24 04:21:23 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
for (int i = r.newest_; r.transfers_[i].date_ > cutoff;)
|
2011-03-15 18:11:31 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
bytes += r.transfers_[i].size_;
|
2011-03-15 18:11:31 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
if (--i == -1)
|
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
i = HistorySize - 1; /* circular history */
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2013-01-24 23:59:52 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (i == r.newest_)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
|
|
|
break; /* we've come all the way around */
|
|
|
|
}
|
2011-03-15 18:11:31 +00:00
|
|
|
}
|
2008-11-24 04:21:23 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
r.cache_val_ = unsigned(bytes * 1000U / interval_msec);
|
|
|
|
r.cache_time_ = now;
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
return r.cache_val_;
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
void Bandwidth::notifyBandwidthConsumedBytes(uint64_t const now, RateControl* r, size_t size)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
if (r->transfers_[r->newest_].date_ + GranularityMSec >= now)
|
2013-01-24 23:59:52 +00:00
|
|
|
{
|
2021-10-10 01:12:03 +00:00
|
|
|
r->transfers_[r->newest_].size_ += size;
|
2013-01-24 23:59:52 +00:00
|
|
|
}
|
2017-04-19 12:04:45 +00:00
|
|
|
else
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
if (++r->newest_ == HistorySize)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-10 01:12:03 +00:00
|
|
|
r->newest_ = 0;
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
r->transfers_[r->newest_].date_ = now;
|
|
|
|
r->transfers_[r->newest_].size_ = size;
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
2011-03-15 18:11:31 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
/* invalidate cache_val*/
|
2021-10-10 01:12:03 +00:00
|
|
|
r->cache_time_ = 0;
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
2008-11-25 21:35:17 +00:00
|
|
|
/***
|
|
|
|
****
|
|
|
|
***/
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
Bandwidth::Bandwidth(Bandwidth* new_parent)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2021-10-10 01:12:03 +00:00
|
|
|
this->band_[TR_UP].honor_parent_limits_ = true;
|
|
|
|
this->band_[TR_DOWN].honor_parent_limits_ = true;
|
2021-10-12 06:04:22 +00:00
|
|
|
this->setParent(new_parent);
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/***
|
|
|
|
****
|
|
|
|
***/
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
void Bandwidth::setParent(Bandwidth* new_parent)
|
2008-11-25 21:35:17 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
TR_ASSERT(this != new_parent);
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
if (this->parent_ != nullptr)
|
2008-11-25 21:35:17 +00:00
|
|
|
{
|
2021-10-10 01:12:03 +00:00
|
|
|
this->parent_->children_.erase(this);
|
|
|
|
this->parent_ = nullptr;
|
2008-11-25 21:35:17 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (new_parent != nullptr)
|
2008-11-25 21:35:17 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
TR_ASSERT(new_parent->parent_ != this);
|
|
|
|
TR_ASSERT(new_parent->children_.find(this) == new_parent->children_.end()); // does not exist
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
new_parent->children_.insert(this);
|
|
|
|
this->parent_ = new_parent;
|
2008-11-25 21:35:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/***
|
|
|
|
****
|
|
|
|
***/
|
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
void Bandwidth::allocateBandwidth(
|
2021-08-15 09:41:48 +00:00
|
|
|
tr_priority_t parent_priority,
|
|
|
|
tr_direction dir,
|
|
|
|
unsigned int period_msec,
|
2021-10-09 12:52:09 +00:00
|
|
|
std::vector<tr_peerIo*>& peer_pool)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
tr_priority_t const priority = std::max(parent_priority, this->priority_);
|
2017-06-13 02:24:09 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
/* set the available bandwidth */
|
2021-10-10 01:12:03 +00:00
|
|
|
if (this->band_[dir].is_limited_)
|
2008-11-26 15:58:26 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
uint64_t const next_pulse_speed = this->band_[dir].desired_speed_bps_;
|
|
|
|
this->band_[dir].bytes_left_ = next_pulse_speed * period_msec / 1000U;
|
2008-11-26 15:58:26 +00:00
|
|
|
}
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
/* add this bandwidth's peer, if any, to the peer pool */
|
2021-10-10 01:12:03 +00:00
|
|
|
if (this->peer_ != nullptr)
|
2013-01-24 23:59:52 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
this->peer_->priority = priority;
|
2021-10-10 01:12:03 +00:00
|
|
|
peer_pool.push_back(this->peer_);
|
2009-04-18 23:17:30 +00:00
|
|
|
}
|
2008-12-09 22:05:45 +00:00
|
|
|
|
2021-10-09 12:52:09 +00:00
|
|
|
// traverse & repeat for the subtree
|
2021-10-12 06:04:22 +00:00
|
|
|
for (auto* child : this->children_)
|
2013-01-24 23:59:52 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
child->allocateBandwidth(priority, dir, period_msec, peer_pool);
|
2008-11-25 21:35:17 +00:00
|
|
|
}
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
void Bandwidth::phaseOne(std::vector<tr_peerIo*>& peerArray, tr_direction dir)
|
2008-12-09 22:05:45 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
/* First phase of IO. Tries to distribute bandwidth fairly to keep faster
|
|
|
|
* peers from starving the others. Loop through the peers, giving each a
|
|
|
|
* small chunk of bandwidth. Keep looping until we run out of bandwidth
|
|
|
|
* and/or peers that can use it */
|
2021-10-09 12:52:09 +00:00
|
|
|
dbgmsg("%lu peers to go round-robin for %s", peerArray.size(), dir == TR_UP ? "upload" : "download");
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2021-10-09 12:52:09 +00:00
|
|
|
size_t n = peerArray.size();
|
2017-04-19 12:04:45 +00:00
|
|
|
while (n > 0)
|
2008-12-15 21:22:08 +00:00
|
|
|
{
|
2017-04-20 16:02:19 +00:00
|
|
|
int const i = tr_rand_int_weak(n); /* pick a peer at random */
|
2011-10-25 16:56:19 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
/* value of 3000 bytes chosen so that when using uTP we'll send a full-size
|
|
|
|
* frame right away and leave enough buffered data for the next frame to go
|
|
|
|
* out in a timely manner. */
|
2017-04-20 16:02:19 +00:00
|
|
|
size_t const increment = 3000;
|
2011-05-04 21:38:01 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
int const bytes_used = tr_peerIoFlush(peerArray[i], dir, increment);
|
2008-12-20 22:19:34 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
dbgmsg("peer #%d of %zu used %d bytes in this pass", i, n, bytes_used);
|
2009-01-03 02:43:17 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (bytes_used != int(increment))
|
2013-01-24 23:59:52 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
/* peer is done writing for now; move it to the end of the list */
|
2021-10-09 12:52:09 +00:00
|
|
|
std::swap(peerArray[i], peerArray[n - 1]);
|
2017-04-19 12:04:45 +00:00
|
|
|
--n;
|
2008-12-15 21:22:08 +00:00
|
|
|
}
|
2008-12-09 22:05:45 +00:00
|
|
|
}
|
2009-04-18 23:17:30 +00:00
|
|
|
}
|
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
void Bandwidth::allocate(tr_direction dir, unsigned int period_msec)
|
2009-04-18 23:17:30 +00:00
|
|
|
{
|
2021-10-22 00:02:38 +00:00
|
|
|
TR_ASSERT(tr_isDirection(dir));
|
|
|
|
|
|
|
|
auto high = std::vector<tr_peerIo*>{};
|
|
|
|
auto low = std::vector<tr_peerIo*>{};
|
|
|
|
auto normal = std::vector<tr_peerIo*>{};
|
|
|
|
auto tmp = std::vector<tr_peerIo*>{};
|
2017-04-19 12:04:45 +00:00
|
|
|
|
|
|
|
/* allocateBandwidth () is a helper function with two purposes:
|
|
|
|
* 1. allocate bandwidth to b and its subtree
|
|
|
|
* 2. accumulate an array of all the peerIos from b and its subtree. */
|
2021-10-09 12:52:09 +00:00
|
|
|
this->allocateBandwidth(TR_PRI_LOW, dir, period_msec, tmp);
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
for (auto* io : tmp)
|
2009-04-21 16:18:51 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
tr_peerIoRef(io);
|
|
|
|
tr_peerIoFlushOutgoingProtocolMsgs(io);
|
2009-04-21 16:18:51 +00:00
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
switch (io->priority)
|
2013-01-24 23:59:52 +00:00
|
|
|
{
|
2017-04-19 12:04:45 +00:00
|
|
|
case TR_PRI_HIGH:
|
2021-10-09 12:52:09 +00:00
|
|
|
high.push_back(io);
|
2021-10-06 17:24:02 +00:00
|
|
|
[[fallthrough]];
|
2017-04-19 12:04:45 +00:00
|
|
|
|
|
|
|
case TR_PRI_NORMAL:
|
2021-10-09 12:52:09 +00:00
|
|
|
normal.push_back(io);
|
2021-10-06 17:24:02 +00:00
|
|
|
[[fallthrough]];
|
2017-04-19 12:04:45 +00:00
|
|
|
|
|
|
|
default:
|
2021-10-09 12:52:09 +00:00
|
|
|
low.push_back(io);
|
2009-04-18 23:17:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-19 12:04:45 +00:00
|
|
|
/* First phase of IO. Tries to distribute bandwidth fairly to keep faster
|
|
|
|
* peers from starving the others. Loop through the peers, giving each a
|
|
|
|
* small chunk of bandwidth. Keep looping until we run out of bandwidth
|
|
|
|
* and/or peers that can use it */
|
2021-10-09 12:52:09 +00:00
|
|
|
phaseOne(high, dir);
|
|
|
|
phaseOne(normal, dir);
|
|
|
|
phaseOne(low, dir);
|
2017-04-19 12:04:45 +00:00
|
|
|
|
|
|
|
/* Second phase of IO. To help us scale in high bandwidth situations,
|
|
|
|
* enable on-demand IO for peers with bandwidth left to burn.
|
|
|
|
* This on-demand IO is enabled until (1) the peer runs out of bandwidth,
|
2021-10-10 01:12:03 +00:00
|
|
|
* or (2) the next Bandwidth::allocate () call, when we start over again. */
|
2021-10-12 06:04:22 +00:00
|
|
|
for (auto* io : tmp)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-09 12:52:09 +00:00
|
|
|
tr_peerIoSetEnabled(io, dir, tr_peerIoHasBandwidthLeft(io, dir));
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
for (auto* io : tmp)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-09 12:52:09 +00:00
|
|
|
tr_peerIoUnref(io);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/***
|
|
|
|
****
|
|
|
|
***/
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
unsigned int Bandwidth::clamp(uint64_t now, tr_direction dir, unsigned int byte_count) const
|
2008-11-25 21:35:17 +00:00
|
|
|
{
|
2017-06-08 07:24:12 +00:00
|
|
|
TR_ASSERT(tr_isDirection(dir));
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
if (this->band_[dir].is_limited_)
|
2008-11-25 21:35:17 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count = std::min(byte_count, this->band_[dir].bytes_left_);
|
2021-10-09 12:52:09 +00:00
|
|
|
|
|
|
|
/* if we're getting close to exceeding the speed limit,
|
|
|
|
* clamp down harder on the bytes available */
|
2021-10-12 06:04:22 +00:00
|
|
|
if (byte_count > 0)
|
2011-03-02 07:21:58 +00:00
|
|
|
{
|
2021-10-09 12:52:09 +00:00
|
|
|
if (now == 0)
|
2011-03-15 18:11:31 +00:00
|
|
|
{
|
2021-10-09 12:52:09 +00:00
|
|
|
now = tr_time_msec();
|
2011-03-15 18:11:31 +00:00
|
|
|
}
|
2011-03-02 07:21:58 +00:00
|
|
|
|
2021-10-23 15:43:15 +00:00
|
|
|
auto const current = this->getRawSpeedBytesPerSecond(now, TR_DOWN);
|
|
|
|
auto const desired = this->getDesiredSpeedBytesPerSecond(TR_DOWN);
|
|
|
|
auto const r = desired >= 1 ? double(current) / desired : 0;
|
2021-10-09 12:52:09 +00:00
|
|
|
|
|
|
|
if (r > 1.0)
|
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count = 0;
|
2021-10-09 12:52:09 +00:00
|
|
|
}
|
|
|
|
else if (r > 0.9)
|
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count = static_cast<unsigned int>(byte_count * 0.8);
|
2021-10-09 12:52:09 +00:00
|
|
|
}
|
|
|
|
else if (r > 0.8)
|
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count = static_cast<unsigned int>(byte_count * 0.9);
|
2021-10-09 12:52:09 +00:00
|
|
|
}
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2008-11-25 21:35:17 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (this->parent_ != nullptr && this->band_[dir].honor_parent_limits_ && byte_count > 0)
|
2021-10-09 12:52:09 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count = this->parent_->clamp(now, dir, byte_count);
|
2021-10-09 12:52:09 +00:00
|
|
|
}
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
return byte_count;
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
void Bandwidth::notifyBandwidthConsumed(tr_direction dir, size_t byte_count, bool is_piece_data, uint64_t now)
|
2008-11-24 04:21:23 +00:00
|
|
|
{
|
2017-06-08 07:24:12 +00:00
|
|
|
TR_ASSERT(tr_isDirection(dir));
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
Band* band = &this->band_[dir];
|
2008-11-24 04:21:23 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (band->is_limited_ && is_piece_data)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
band->bytes_left_ -= std::min(size_t{ band->bytes_left_ }, byte_count);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2008-11-25 21:35:17 +00:00
|
|
|
|
|
|
|
#ifdef DEBUG_DIRECTION
|
2017-04-19 12:04:45 +00:00
|
|
|
|
2021-10-09 12:52:09 +00:00
|
|
|
if (dir == DEBUG_DIRECTION && band_->isLimited)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-08-15 09:41:48 +00:00
|
|
|
fprintf(
|
|
|
|
stderr,
|
|
|
|
"%p consumed %5zu bytes of %5s data... was %6zu, now %6zu left\n",
|
2021-10-09 12:52:09 +00:00
|
|
|
this,
|
2021-10-12 06:04:22 +00:00
|
|
|
byte_count,
|
|
|
|
is_piece_data ? "piece" : "raw",
|
2021-08-15 09:41:48 +00:00
|
|
|
oldBytesLeft,
|
2021-10-09 12:52:09 +00:00
|
|
|
band_->bytesLeft);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
|
|
|
|
2008-11-25 21:35:17 +00:00
|
|
|
#endif
|
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
notifyBandwidthConsumedBytes(now, &band->raw_, byte_count);
|
2008-11-24 04:21:23 +00:00
|
|
|
|
2021-10-12 06:04:22 +00:00
|
|
|
if (is_piece_data)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
notifyBandwidthConsumedBytes(now, &band->piece_, byte_count);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2008-11-25 21:35:17 +00:00
|
|
|
|
2021-10-10 01:12:03 +00:00
|
|
|
if (this->parent_ != nullptr)
|
2017-04-19 12:04:45 +00:00
|
|
|
{
|
2021-10-12 06:04:22 +00:00
|
|
|
this->parent_->notifyBandwidthConsumed(dir, byte_count, is_piece_data, now);
|
2017-04-19 12:04:45 +00:00
|
|
|
}
|
2008-11-24 04:21:23 +00:00
|
|
|
}
|