Change file format for fee estimates.

Move buckets and bucketMap to be stored as part of overall serialization of estimator.
Add some placeholder data so file format is only changed once.
Maintain 3 different TxConfirmStats with potential for different decays and scales.
This commit is contained in:
Alex Morcos 2017-02-23 15:13:41 -05:00
parent 14c948987f
commit c0a273f4c8
2 changed files with 160 additions and 81 deletions

View File

@ -26,8 +26,8 @@ class TxConfirmStats
{ {
private: private:
//Define the buckets we will group transactions into //Define the buckets we will group transactions into
std::vector<double> buckets; // The upper-bound of the range for the bucket (inclusive) const std::vector<double>& buckets; // The upper-bound of the range for the bucket (inclusive)
std::map<double, unsigned int> bucketMap; // Map of bucket upper-bound to index into all vectors by bucket const std::map<double, unsigned int>& bucketMap; // Map of bucket upper-bound to index into all vectors by bucket
// For each bucket X: // For each bucket X:
// Count the total # of txs in each bucket // Count the total # of txs in each bucket
@ -38,9 +38,11 @@ private:
// Count the total # of txs confirmed within Y blocks in each bucket // Count the total # of txs confirmed within Y blocks in each bucket
// Track the historical moving average of theses totals over blocks // Track the historical moving average of theses totals over blocks
std::vector<std::vector<double> > confAvg; // confAvg[Y][X] std::vector<std::vector<double>> confAvg; // confAvg[Y][X]
// and calculate the totals for the current block to update the moving averages // and calculate the totals for the current block to update the moving averages
std::vector<std::vector<int> > curBlockConf; // curBlockConf[Y][X] std::vector<std::vector<int>> curBlockConf; // curBlockConf[Y][X]
std::vector<std::vector<double>> failAvg; // future use
// Sum the total feerate of all tx's in each bucket // Sum the total feerate of all tx's in each bucket
// Track the historical moving average of this total over blocks // Track the historical moving average of this total over blocks
@ -53,6 +55,8 @@ private:
double decay; double decay;
unsigned int scale;
// Mempool counts of outstanding transactions // Mempool counts of outstanding transactions
// For each bucket X, track the number of transactions in the mempool // For each bucket X, track the number of transactions in the mempool
// that are unconfirmed for each possible confirmation value Y // that are unconfirmed for each possible confirmation value Y
@ -60,6 +64,8 @@ private:
// transactions still unconfirmed after MAX_CONFIRMS for each bucket // transactions still unconfirmed after MAX_CONFIRMS for each bucket
std::vector<int> oldUnconfTxs; std::vector<int> oldUnconfTxs;
void resizeInMemoryCounters(size_t newbuckets);
public: public:
/** /**
* Create new TxConfirmStats. This is called by BlockPolicyEstimator's * Create new TxConfirmStats. This is called by BlockPolicyEstimator's
@ -68,7 +74,8 @@ public:
* @param maxConfirms max number of confirms to track * @param maxConfirms max number of confirms to track
* @param decay how much to decay the historical moving average per block * @param decay how much to decay the historical moving average per block
*/ */
TxConfirmStats(const std::vector<double>& defaultBuckets, unsigned int maxConfirms, double decay); TxConfirmStats(const std::vector<double>& defaultBuckets, const std::map<double, unsigned int>& defaultBucketMap,
unsigned int maxConfirms, double decay);
/** Clear the state of the curBlock variables to start counting for the new block */ /** Clear the state of the curBlock variables to start counting for the new block */
void ClearCurrent(unsigned int nBlockHeight); void ClearCurrent(unsigned int nBlockHeight);
@ -116,32 +123,39 @@ public:
* Read saved state of estimation data from a file and replace all internal data structures and * Read saved state of estimation data from a file and replace all internal data structures and
* variables with this state. * variables with this state.
*/ */
void Read(CAutoFile& filein); void Read(CAutoFile& filein, int nFileVersion, size_t numBuckets);
}; };
TxConfirmStats::TxConfirmStats(const std::vector<double>& defaultBuckets, TxConfirmStats::TxConfirmStats(const std::vector<double>& defaultBuckets,
const std::map<double, unsigned int>& defaultBucketMap,
unsigned int maxConfirms, double _decay) unsigned int maxConfirms, double _decay)
: buckets(defaultBuckets), bucketMap(defaultBucketMap)
{ {
decay = _decay; decay = _decay;
for (unsigned int i = 0; i < defaultBuckets.size(); i++) { scale = 1;
buckets.push_back(defaultBuckets[i]);
bucketMap[defaultBuckets[i]] = i;
}
confAvg.resize(maxConfirms); confAvg.resize(maxConfirms);
curBlockConf.resize(maxConfirms);
unconfTxs.resize(maxConfirms);
for (unsigned int i = 0; i < maxConfirms; i++) { for (unsigned int i = 0; i < maxConfirms; i++) {
confAvg[i].resize(buckets.size()); confAvg[i].resize(buckets.size());
curBlockConf[i].resize(buckets.size());
unconfTxs[i].resize(buckets.size());
} }
oldUnconfTxs.resize(buckets.size());
curBlockTxCt.resize(buckets.size());
txCtAvg.resize(buckets.size()); txCtAvg.resize(buckets.size());
curBlockVal.resize(buckets.size());
avg.resize(buckets.size()); avg.resize(buckets.size());
resizeInMemoryCounters(buckets.size());
}
void TxConfirmStats::resizeInMemoryCounters(size_t newbuckets) {
curBlockConf.resize(GetMaxConfirms());
// newbuckets must be passed in because the buckets referred to during Read have not been updated yet.
unconfTxs.resize(GetMaxConfirms());
for (unsigned int i = 0; i < unconfTxs.size(); i++) {
curBlockConf[i].resize(newbuckets);
unconfTxs[i].resize(newbuckets);
}
oldUnconfTxs.resize(newbuckets);
curBlockTxCt.resize(newbuckets);
curBlockVal.resize(newbuckets);
} }
// Zero out the data for the current block // Zero out the data for the current block
@ -283,70 +297,55 @@ double TxConfirmStats::EstimateMedianVal(int confTarget, double sufficientTxVal,
void TxConfirmStats::Write(CAutoFile& fileout) const void TxConfirmStats::Write(CAutoFile& fileout) const
{ {
fileout << decay; fileout << decay;
fileout << buckets; fileout << scale;
fileout << avg; fileout << avg;
fileout << txCtAvg; fileout << txCtAvg;
fileout << confAvg; fileout << confAvg;
fileout << failAvg;
} }
void TxConfirmStats::Read(CAutoFile& filein) void TxConfirmStats::Read(CAutoFile& filein, int nFileVersion, size_t numBuckets)
{ {
// Read data file into temporary variables and do some very basic sanity checking // Read data file and do some very basic sanity checking
std::vector<double> fileBuckets; // buckets and bucketMap are not updated yet, so don't access them
std::vector<double> fileAvg; // If there is a read failure, we'll just discard this entire object anyway
std::vector<std::vector<double> > fileConfAvg;
std::vector<double> fileTxCtAvg;
double fileDecay;
size_t maxConfirms; size_t maxConfirms;
size_t numBuckets;
filein >> fileDecay; // The current version will store the decay with each individual TxConfirmStats and also keep a scale factor
if (fileDecay <= 0 || fileDecay >= 1) if (nFileVersion >= 149900) {
filein >> decay;
if (decay <= 0 || decay >= 1) {
throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)"); throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)");
filein >> fileBuckets; }
numBuckets = fileBuckets.size(); filein >> scale; //Unused for now
if (numBuckets <= 1 || numBuckets > 1000) }
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
filein >> fileAvg; filein >> avg;
if (fileAvg.size() != numBuckets) if (avg.size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in feerate average bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in feerate average bucket count");
filein >> fileTxCtAvg; }
if (fileTxCtAvg.size() != numBuckets) filein >> txCtAvg;
if (txCtAvg.size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in tx count bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in tx count bucket count");
filein >> fileConfAvg; }
maxConfirms = fileConfAvg.size(); filein >> confAvg;
if (maxConfirms <= 0 || maxConfirms > 6 * 24 * 7) // one week maxConfirms = confAvg.size();
if (maxConfirms <= 0 || maxConfirms > 6 * 24 * 7) { // one week
throw std::runtime_error("Corrupt estimates file. Must maintain estimates for between 1 and 1008 (one week) confirms"); throw std::runtime_error("Corrupt estimates file. Must maintain estimates for between 1 and 1008 (one week) confirms");
}
for (unsigned int i = 0; i < maxConfirms; i++) { for (unsigned int i = 0; i < maxConfirms; i++) {
if (fileConfAvg[i].size() != numBuckets) if (confAvg[i].size() != numBuckets) {
throw std::runtime_error("Corrupt estimates file. Mismatch in feerate conf average bucket count"); throw std::runtime_error("Corrupt estimates file. Mismatch in feerate conf average bucket count");
} }
// Now that we've processed the entire feerate estimate data file and not }
// thrown any errors, we can copy it to our data structures
decay = fileDecay; if (nFileVersion >= 149900) {
buckets = fileBuckets; filein >> failAvg;
avg = fileAvg; }
confAvg = fileConfAvg;
txCtAvg = fileTxCtAvg;
bucketMap.clear();
// Resize the current block variables which aren't stored in the data file // Resize the current block variables which aren't stored in the data file
// to match the number of confirms and buckets // to match the number of confirms and buckets
curBlockConf.resize(maxConfirms); resizeInMemoryCounters(numBuckets);
for (unsigned int i = 0; i < maxConfirms; i++) {
curBlockConf[i].resize(buckets.size());
}
curBlockTxCt.resize(buckets.size());
curBlockVal.resize(buckets.size());
unconfTxs.resize(maxConfirms);
for (unsigned int i = 0; i < maxConfirms; i++) {
unconfTxs[i].resize(buckets.size());
}
oldUnconfTxs.resize(buckets.size());
for (unsigned int i = 0; i < buckets.size(); i++)
bucketMap[buckets[i]] = i;
LogPrint(BCLog::ESTIMATEFEE, "Reading estimates: %u buckets counting confirms up to %u blocks\n", LogPrint(BCLog::ESTIMATEFEE, "Reading estimates: %u buckets counting confirms up to %u blocks\n",
numBuckets, maxConfirms); numBuckets, maxConfirms);
@ -413,17 +412,25 @@ CBlockPolicyEstimator::CBlockPolicyEstimator()
{ {
static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero"); static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero");
minTrackedFee = CFeeRate(MIN_BUCKET_FEERATE); minTrackedFee = CFeeRate(MIN_BUCKET_FEERATE);
std::vector<double> vfeelist; size_t bucketIndex = 0;
for (double bucketBoundary = minTrackedFee.GetFeePerK(); bucketBoundary <= MAX_BUCKET_FEERATE; bucketBoundary *= FEE_SPACING) { for (double bucketBoundary = minTrackedFee.GetFeePerK(); bucketBoundary <= MAX_BUCKET_FEERATE; bucketBoundary *= FEE_SPACING, bucketIndex++) {
vfeelist.push_back(bucketBoundary); buckets.push_back(bucketBoundary);
bucketMap[bucketBoundary] = bucketIndex;
} }
vfeelist.push_back(INF_FEERATE); buckets.push_back(INF_FEERATE);
feeStats = new TxConfirmStats(vfeelist, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY); bucketMap[INF_FEERATE] = bucketIndex;
assert(bucketMap.size() == buckets.size());
feeStats = new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY);
shortStats = new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY);
longStats = new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY);
} }
CBlockPolicyEstimator::~CBlockPolicyEstimator() CBlockPolicyEstimator::~CBlockPolicyEstimator()
{ {
delete feeStats; delete feeStats;
delete shortStats;
delete longStats;
} }
void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate) void CBlockPolicyEstimator::processTransaction(const CTxMemPoolEntry& entry, bool validFeeEstimate)
@ -580,10 +587,15 @@ bool CBlockPolicyEstimator::Write(CAutoFile& fileout) const
{ {
try { try {
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
fileout << 139900; // version required to read: 0.13.99 or later fileout << 149900; // version required to read: 0.14.99 or later
fileout << CLIENT_VERSION; // version that wrote the file fileout << CLIENT_VERSION; // version that wrote the file
fileout << nBestSeenHeight; fileout << nBestSeenHeight;
unsigned int future1 = 0, future2 = 0;
fileout << future1 << future2;
fileout << buckets;
feeStats->Write(fileout); feeStats->Write(fileout);
shortStats->Write(fileout);
longStats->Write(fileout);
} }
catch (const std::exception&) { catch (const std::exception&) {
LogPrintf("CBlockPolicyEstimator::Write(): unable to read policy estimator data (non-fatal)\n"); LogPrintf("CBlockPolicyEstimator::Write(): unable to read policy estimator data (non-fatal)\n");
@ -596,17 +608,79 @@ bool CBlockPolicyEstimator::Read(CAutoFile& filein)
{ {
try { try {
LOCK(cs_feeEstimator); LOCK(cs_feeEstimator);
int nVersionRequired, nVersionThatWrote, nFileBestSeenHeight; int nVersionRequired, nVersionThatWrote;
unsigned int nFileBestSeenHeight;
filein >> nVersionRequired >> nVersionThatWrote; filein >> nVersionRequired >> nVersionThatWrote;
if (nVersionRequired > CLIENT_VERSION) if (nVersionRequired > CLIENT_VERSION)
return error("CBlockPolicyEstimator::Read(): up-version (%d) fee estimate file", nVersionRequired); return error("CBlockPolicyEstimator::Read(): up-version (%d) fee estimate file", nVersionRequired);
// Read fee estimates file into temporary variables so existing data
// structures aren't corrupted if there is an exception.
filein >> nFileBestSeenHeight; filein >> nFileBestSeenHeight;
feeStats->Read(filein);
nBestSeenHeight = nFileBestSeenHeight; if (nVersionThatWrote < 149900) {
// Read the old fee estimates file for temporary use, but then discard. Will start collecting data from scratch.
// decay is stored before buckets in old versions, so pre-read decay and pass into TxConfirmStats constructor
double tempDecay;
filein >> tempDecay;
if (tempDecay <= 0 || tempDecay >= 1)
throw std::runtime_error("Corrupt estimates file. Decay must be between 0 and 1 (non-inclusive)");
std::vector<double> tempBuckets;
filein >> tempBuckets;
size_t tempNum = tempBuckets.size();
if (tempNum <= 1 || tempNum > 1000)
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
std::map<double, unsigned int> tempMap;
std::unique_ptr<TxConfirmStats> tempFeeStats(new TxConfirmStats(tempBuckets, tempMap, MAX_BLOCK_CONFIRMS, tempDecay));
tempFeeStats->Read(filein, nVersionThatWrote, tempNum);
// if nVersionThatWrote < 139900 then another TxConfirmStats (for priority) follows but can be ignored. // if nVersionThatWrote < 139900 then another TxConfirmStats (for priority) follows but can be ignored.
tempMap.clear();
for (unsigned int i = 0; i < tempBuckets.size(); i++) {
tempMap[tempBuckets[i]] = i;
} }
catch (const std::exception&) { }
LogPrintf("CBlockPolicyEstimator::Read(): unable to read policy estimator data (non-fatal)\n"); else { // nVersionThatWrote >= 149900
unsigned int future1, future2;
filein >> future1 >> future2;
std::vector<double> fileBuckets;
filein >> fileBuckets;
size_t numBuckets = fileBuckets.size();
if (numBuckets <= 1 || numBuckets > 1000)
throw std::runtime_error("Corrupt estimates file. Must have between 2 and 1000 feerate buckets");
std::unique_ptr<TxConfirmStats> fileFeeStats(new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY));
std::unique_ptr<TxConfirmStats> fileShortStats(new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY));
std::unique_ptr<TxConfirmStats> fileLongStats(new TxConfirmStats(buckets, bucketMap, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY));
fileFeeStats->Read(filein, nVersionThatWrote, numBuckets);
fileShortStats->Read(filein, nVersionThatWrote, numBuckets);
fileLongStats->Read(filein, nVersionThatWrote, numBuckets);
// Fee estimates file parsed correctly
// Copy buckets from file and refresh our bucketmap
buckets = fileBuckets;
bucketMap.clear();
for (unsigned int i = 0; i < buckets.size(); i++) {
bucketMap[buckets[i]] = i;
}
// Destroy old TxConfirmStats and point to new ones that already reference buckets and bucketMap
delete feeStats;
delete shortStats;
delete longStats;
feeStats = fileFeeStats.release();
shortStats = fileShortStats.release();
longStats = fileLongStats.release();
nBestSeenHeight = nFileBestSeenHeight;
}
}
catch (const std::exception& e) {
LogPrintf("CBlockPolicyEstimator::Read(): unable to read policy estimator data (non-fatal): %s\n",e.what());
return false; return false;
} }
return true; return true;

View File

@ -141,10 +141,15 @@ private:
/** Classes to track historical data on transaction confirmations */ /** Classes to track historical data on transaction confirmations */
TxConfirmStats* feeStats; TxConfirmStats* feeStats;
TxConfirmStats* shortStats;
TxConfirmStats* longStats;
unsigned int trackedTxs; unsigned int trackedTxs;
unsigned int untrackedTxs; unsigned int untrackedTxs;
std::vector<double> buckets; // The upper-bound of the range for the bucket (inclusive)
std::map<double, unsigned int> bucketMap; // Map of bucket upper-bound to index into all vectors by bucket
mutable CCriticalSection cs_feeEstimator; mutable CCriticalSection cs_feeEstimator;
/** Process a transaction confirmed in a block*/ /** Process a transaction confirmed in a block*/