-
Notifications
You must be signed in to change notification settings - Fork 1.4k
First-pass IMT implementation of FlushBaskets. #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
c247dcc
0be50e3
88def72
9235428
249cb14
e14e674
71b2161
cca9e30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -70,6 +70,8 @@ | |
| #include "TVirtualTreePlayer.h" | ||
| #endif | ||
|
|
||
| #include <mutex> | ||
|
||
|
|
||
| class TBranch; | ||
| class TBrowser; | ||
| class TFile; | ||
|
|
@@ -99,6 +101,7 @@ class TTree : public TNamed, public TAttLine, public TAttFill, public TAttMarker | |
|
|
||
| protected: | ||
| Long64_t fEntries; ///< Number of entries | ||
| // NOTE: cannot use std::atomic for these counters as it cannot be serialized. | ||
| Long64_t fTotBytes; ///< Total number of bytes in all branches before compression | ||
| Long64_t fZipBytes; ///< Total number of bytes in all branches after compression | ||
| Long64_t fSavedBytes; ///< Number of autosaved bytes | ||
|
|
@@ -160,6 +163,9 @@ class TTree : public TNamed, public TAttLine, public TAttFill, public TAttMarker | |
| TTree(const TTree& tt); // not implemented | ||
| TTree& operator=(const TTree& tt); // not implemented | ||
|
|
||
| #ifdef R__USE_IMT | ||
| mutable std::mutex fCounterMutex; ///<!Lock to protect counters | ||
|
||
| #endif | ||
| void InitializeSortedBranches(); | ||
| void SortBranchesByTime(); | ||
|
|
||
|
|
@@ -303,8 +309,8 @@ class TTree : public TNamed, public TAttLine, public TAttFill, public TAttMarker | |
| virtual TFriendElement *AddFriend(const char* treename, const char* filename = ""); | ||
| virtual TFriendElement *AddFriend(const char* treename, TFile* file); | ||
| virtual TFriendElement *AddFriend(TTree* tree, const char* alias = "", Bool_t warn = kFALSE); | ||
| virtual void AddTotBytes(Int_t tot) { fTotBytes += tot; } | ||
| virtual void AddZipBytes(Int_t zip) { fZipBytes += zip; } | ||
| virtual void AddTotBytes(Int_t tot) { std::lock_guard<std::mutex> sentry(fCounterMutex); fTotBytes += tot; } | ||
|
||
| virtual void AddZipBytes(Int_t zip) { std::lock_guard<std::mutex> sentry(fCounterMutex); fZipBytes += zip; } | ||
| virtual Long64_t AutoSave(Option_t* option = ""); | ||
| virtual Int_t Branch(TCollection* list, Int_t bufsize = 32000, Int_t splitlevel = 99, const char* name = ""); | ||
| virtual Int_t Branch(TList* list, Int_t bufsize = 32000, Int_t splitlevel = 99); | ||
|
|
@@ -439,7 +445,7 @@ class TTree : public TNamed, public TAttLine, public TAttFill, public TAttMarker | |
| virtual Long64_t GetSelectedRows() { return GetPlayer()->GetSelectedRows(); } | ||
| virtual Int_t GetTimerInterval() const { return fTimerInterval; } | ||
| TBuffer* GetTransientBuffer(Int_t size); | ||
| virtual Long64_t GetTotBytes() const { return fTotBytes; } | ||
| virtual Long64_t GetTotBytes() const { std::lock_guard<std::mutex> sentry(fCounterMutex); return fTotBytes; } | ||
|
||
| virtual TTree *GetTree() const { return const_cast<TTree*>(this); } | ||
| virtual TVirtualIndex *GetTreeIndex() const { return fTreeIndex; } | ||
| virtual Int_t GetTreeNumber() const { return 0; } | ||
|
|
@@ -467,7 +473,7 @@ class TTree : public TNamed, public TAttLine, public TAttFill, public TAttMarker | |
| virtual Double_t *GetV4() { return GetPlayer()->GetV4(); } | ||
| virtual Double_t *GetW() { return GetPlayer()->GetW(); } | ||
| virtual Double_t GetWeight() const { return fWeight; } | ||
| virtual Long64_t GetZipBytes() const { return fZipBytes; } | ||
| virtual Long64_t GetZipBytes() const { std::lock_guard<std::mutex> sentry(fCounterMutex); return fZipBytes; } | ||
| virtual void IncrementTotalBuffers(Int_t nbytes) { fTotalBuffers += nbytes; } | ||
| Bool_t IsFolder() const { return kTRUE; } | ||
| virtual Int_t LoadBaskets(Long64_t maxmemory = 2000000000); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -932,6 +932,9 @@ Int_t TBasket::WriteBuffer() | |
| return -1; | ||
| } | ||
| fMotherDir = file; // fBranch->GetDirectory(); | ||
| #ifdef R__USE_IMT | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the slightly unusual use pattern (take the lock then explicit unlock then lock). Can you add comment on what this is propecting (and why it will not provoke a dead lock). |
||
| std::unique_lock<std::mutex> sentry(file->fWriteMutex); | ||
| #endif // R__USE_IMT | ||
|
|
||
| if (R__unlikely(fBufferRef->TestBit(TBufferFile::kNotDecompressed))) { | ||
| // Read the basket information that was saved inside the buffer. | ||
|
|
@@ -998,7 +1001,16 @@ Int_t TBasket::WriteBuffer() | |
| if (i == nbuffers - 1) bufmax = fObjlen - nzip; | ||
| else bufmax = kMAXZIPBUF; | ||
| //compress the buffer | ||
| #ifdef R__USE_IMT | ||
| sentry.unlock(); | ||
| #endif // R__USE_IMT | ||
| // NOTE this is declared with C linkage, so it shouldn't except. Also, when | ||
| // USE_IMT is defined, we are guaranteed that the compression buffer is unique per-branch. | ||
| // (see fCompressedBufferRef in constructor). | ||
| R__zipMultipleAlgorithm(cxlevel, &bufmax, objbuf, &bufmax, bufcur, &nout, cxAlgorithm); | ||
| #ifdef R__USE_IMT | ||
| sentry.lock(); | ||
| #endif // R__USE_IMT | ||
|
|
||
| // test if buffer has really been compressed. In case of small buffers | ||
| // when the buffer contains random data, it may happen that the compressed | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1336,7 +1336,7 @@ Long64_t TTree::AutoSave(Option_t* option) | |
| { | ||
| if (!fDirectory || fDirectory == gROOT || !fDirectory->IsWritable()) return 0; | ||
| if (gDebug > 0) { | ||
| printf("AutoSave Tree:%s after %lld bytes written\n",GetName(),fTotBytes); | ||
| printf("AutoSave Tree:%s after %lld bytes written\n",GetName(),GetTotBytes()); | ||
|
||
| } | ||
| TString opt = option; | ||
| opt.ToLower(); | ||
|
|
@@ -1346,7 +1346,7 @@ Long64_t TTree::AutoSave(Option_t* option) | |
| FlushBaskets(); | ||
| } | ||
|
|
||
| fSavedBytes = fZipBytes; | ||
| fSavedBytes = GetZipBytes(); | ||
|
|
||
| TKey *key = (TKey*)fDirectory->GetListOfKeys()->FindObject(GetName()); | ||
| Long64_t nbytes; | ||
|
|
@@ -4418,34 +4418,36 @@ Int_t TTree::Fill() | |
| KeepCircular(); | ||
| } | ||
| if (gDebug > 0) printf("TTree::Fill - A: %d %lld %lld %lld %lld %lld %lld \n", | ||
| nbytes, fEntries, fAutoFlush,fAutoSave,fZipBytes,fFlushedBytes,fSavedBytes); | ||
| nbytes, fEntries, fAutoFlush,fAutoSave,GetZipBytes(),fFlushedBytes,fSavedBytes); | ||
|
|
||
| if (fAutoFlush != 0 || fAutoSave != 0) { | ||
| // Is it time to flush or autosave baskets? | ||
| if (fFlushedBytes == 0) { | ||
| // Decision can be based initially either on the number of bytes | ||
| // or the number of entries written. | ||
| if ((fAutoFlush<0 && fZipBytes > -fAutoFlush) || | ||
| (fAutoSave <0 && fZipBytes > -fAutoSave ) || | ||
| Long64_t zipBytes = GetZipBytes(); | ||
| if ((fAutoFlush<0 && zipBytes > -fAutoFlush) || | ||
| (fAutoSave <0 && zipBytes > -fAutoSave ) || | ||
| (fAutoFlush>0 && fEntries%TMath::Max((Long64_t)1,fAutoFlush) == 0) || | ||
| (fAutoSave >0 && fEntries%TMath::Max((Long64_t)1,fAutoSave) == 0) ) { | ||
|
|
||
| //First call FlushBasket to make sure that fTotBytes is up to date. | ||
| FlushBaskets(); | ||
| OptimizeBaskets(fTotBytes,1,""); | ||
| if (gDebug > 0) Info("TTree::Fill","OptimizeBaskets called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,fZipBytes,fFlushedBytes); | ||
| fFlushedBytes = fZipBytes; | ||
| OptimizeBaskets(GetTotBytes(),1,""); | ||
| if (gDebug > 0) Info("TTree::Fill","OptimizeBaskets called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,GetZipBytes(),fFlushedBytes); | ||
| fFlushedBytes = GetZipBytes(); | ||
| fAutoFlush = fEntries; // Use test on entries rather than bytes | ||
|
|
||
| // subsequently in run | ||
| if (fAutoSave < 0) { | ||
| // Set fAutoSave to the largest integer multiple of | ||
| // fAutoFlush events such that fAutoSave*fFlushedBytes | ||
| // < (minus the input value of fAutoSave) | ||
| if (fZipBytes != 0) { | ||
| fAutoSave = TMath::Max( fAutoFlush, fEntries*((-fAutoSave/fZipBytes)/fEntries)); | ||
| } else if (fTotBytes != 0) { | ||
| fAutoSave = TMath::Max( fAutoFlush, fEntries*((-fAutoSave/fTotBytes)/fEntries)); | ||
| Long64_t totBytes = GetTotBytes(); | ||
| if (zipBytes != 0) { | ||
| fAutoSave = TMath::Max( fAutoFlush, fEntries*((-fAutoSave/zipBytes)/fEntries)); | ||
| } else if (totBytes != 0) { | ||
| fAutoSave = TMath::Max( fAutoFlush, fEntries*((-fAutoSave/totBytes)/fEntries)); | ||
| } else { | ||
| TBufferFile b(TBuffer::kWrite, 10000); | ||
| TTree::Class()->WriteBuffer(b, (TTree*) this); | ||
|
|
@@ -4462,24 +4464,24 @@ Int_t TTree::Fill() | |
| if (fAutoSave != 0 && fEntries%fAutoSave == 0) { | ||
| //We are at an AutoSave point. AutoSave flushes baskets and saves the Tree header | ||
| AutoSave("flushbaskets"); | ||
| if (gDebug > 0) Info("TTree::Fill","AutoSave called at entry %lld, fZipBytes=%lld, fSavedBytes=%lld\n",fEntries,fZipBytes,fSavedBytes); | ||
| if (gDebug > 0) Info("TTree::Fill","AutoSave called at entry %lld, fZipBytes=%lld, fSavedBytes=%lld\n",fEntries,GetZipBytes(),fSavedBytes); | ||
| } else { | ||
| //We only FlushBaskets | ||
| FlushBaskets(); | ||
| if (gDebug > 0) Info("TTree::Fill","FlushBasket called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,fZipBytes,fFlushedBytes); | ||
| if (gDebug > 0) Info("TTree::Fill","FlushBasket called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,GetZipBytes(),fFlushedBytes); | ||
| } | ||
| fFlushedBytes = fZipBytes; | ||
| fFlushedBytes = GetZipBytes(); | ||
| } else if (fNClusterRange == 0 && fEntries > 1 && fAutoFlush && fEntries%fAutoFlush == 0) { | ||
| if (fAutoSave != 0 && fEntries%fAutoSave == 0) { | ||
| //We are at an AutoSave point. AutoSave flushes baskets and saves the Tree header | ||
| AutoSave("flushbaskets"); | ||
| if (gDebug > 0) Info("TTree::Fill","AutoSave called at entry %lld, fZipBytes=%lld, fSavedBytes=%lld\n",fEntries,fZipBytes,fSavedBytes); | ||
| if (gDebug > 0) Info("TTree::Fill","AutoSave called at entry %lld, fZipBytes=%lld, fSavedBytes=%lld\n",fEntries,GetZipBytes(),fSavedBytes); | ||
| } else { | ||
| //We only FlushBaskets | ||
| FlushBaskets(); | ||
| if (gDebug > 0) Info("TTree::Fill","FlushBasket called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,fZipBytes,fFlushedBytes); | ||
| if (gDebug > 0) Info("TTree::Fill","FlushBasket called at entry %lld, fZipBytes=%lld, fFlushedBytes=%lld\n",fEntries,GetZipBytes(),fFlushedBytes); | ||
| } | ||
| fFlushedBytes = fZipBytes; | ||
| fFlushedBytes = GetZipBytes(); | ||
| } | ||
| } | ||
| // Check that output file is still below the maximum size. | ||
|
|
@@ -4802,6 +4804,48 @@ Int_t TTree::FlushBaskets() const | |
| Int_t nerror = 0; | ||
| TObjArray *lb = const_cast<TTree*>(this)->GetListOfBranches(); | ||
| Int_t nb = lb->GetEntriesFast(); | ||
|
|
||
| #ifdef R__USE_IMT | ||
| if (ROOT::IsImplicitMTEnabled() && fIMTEnabled) { | ||
| if (fSortedBranches.empty()) { const_cast<TTree*>(this)->InitializeSortedBranches(); } | ||
|
|
||
| // Enable this IMT use case (activate its locks) | ||
| ROOT::Internal::TParBranchProcessingRAII pbpRAII; | ||
|
|
||
| std::atomic<Int_t> nerrpar(0); | ||
| std::atomic<Int_t> nbpar(0); | ||
| std::atomic<Int_t> pos(0); | ||
| tbb::task_group g; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must be extremely careful when embedding calls to TBB into legacy code. Holding any shared resource (which a C++ object itself may be viewed as such a resource) can lead to deadlocks. See https://software.intel.com/en-us/forums/intel-threading-building-blocks/topic/401006 for details. This is one reason why the CMS threaded-framework is built using explicit tasks rather than implicit ones. For the case of writing, I can't think of anyone being dependent on the TFile, although we must be certain that no ROOT locks are being held by the thread calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi Chris, Indeed. My logic here is:
So, the remaining danger is if the caller (CMSSW) is holding a mutex of its own that other CMSSW tasks might need. I will write these concerns up into a comment, provide an example of dangerous behavior, and add them to the function's documentation. Brian
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an example of a deadlock that could have happened in older versions of CMSSW:
Now, Chris tells me that CMSSW has switched away from the mutex for output module access - but this might be a good example. I pushed a long comment for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This of course depend on whether the mutex that is used is recursive or not (TMutex is, std::mutex is not).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Certainly! The point is that working code with IMT-disabled may stop working when IMT is enabled. One would hope that issues would be fixable once the user understands the underlying issue. Hence the extensive comment in the code. |
||
|
|
||
| for (Int_t i = 0; i < nb; i++) { | ||
| g.run([&]() { | ||
| // The branch to process is obtained when the task starts to run. | ||
| // This way, since branches are sorted, we make sure that branches | ||
| // leading to big tasks are processed first. If we assigned the | ||
| // branch at task creation time, the scheduler would not necessarily | ||
| // respect our sorting. | ||
| Int_t j = pos.fetch_add(1); | ||
|
|
||
| auto branch = fSortedBranches[j].second; | ||
| if (R__unlikely(!branch)) { return; } | ||
|
|
||
| if (R__unlikely(gDebug > 0)) { | ||
| std::stringstream ss; | ||
| ss << std::this_thread::get_id(); | ||
| Info("FlushBaskets", "[IMT] Thread %s", ss.str().c_str()); | ||
| Info("FlushBaskets", "[IMT] Running task for branch #%d: %s", j, branch->GetName()); | ||
| } | ||
|
|
||
| Int_t nbtask = branch->FlushBaskets(); | ||
|
|
||
| if (nbtask < 0) { nerrpar++; } | ||
| else { nbpar += nbtask; } | ||
| }); | ||
| } | ||
| g.wait(); | ||
| return nerrpar ? -1 : nbpar.load(); | ||
| } | ||
| #endif | ||
| for (Int_t j = 0; j < nb; j++) { | ||
| TBranch* branch = (TBranch*) lb->UncheckedAt(j); | ||
| if (branch) { | ||
|
|
@@ -5006,7 +5050,7 @@ Long64_t TTree::GetCacheAutoSize(Bool_t withDefault /* = kFALSE */ ) const | |
|
|
||
| if (fAutoFlush < 0) cacheSize = Long64_t(-cacheFactor*fAutoFlush); | ||
| else if (fAutoFlush == 0) cacheSize = 0; | ||
| else cacheSize = Long64_t(cacheFactor*1.5*fAutoFlush*fZipBytes/(fEntries+1)); | ||
| else cacheSize = Long64_t(cacheFactor*1.5*fAutoFlush*GetZipBytes()/(fEntries+1)); | ||
|
|
||
| if (cacheSize >= (INT_MAX / 4)) { | ||
| cacheSize = INT_MAX / 4; | ||
|
|
@@ -5019,7 +5063,7 @@ Long64_t TTree::GetCacheAutoSize(Bool_t withDefault /* = kFALSE */ ) const | |
| if (cacheSize == 0 && withDefault) { | ||
| if (fAutoFlush < 0) cacheSize = -fAutoFlush; | ||
| else if (fAutoFlush == 0) cacheSize = 0; | ||
| else cacheSize = Long64_t(1.5*fAutoFlush*fZipBytes/(fEntries+1)); | ||
| else cacheSize = Long64_t(1.5*fAutoFlush*GetZipBytes()/(fEntries+1)); | ||
| } | ||
|
|
||
| return cacheSize; | ||
|
|
@@ -6702,16 +6746,17 @@ void TTree::Print(Option_t* option) const | |
| } | ||
| } | ||
| Long64_t total = skey; | ||
| if (fZipBytes > 0) { | ||
| total += fTotBytes; | ||
| Long64_t zipBytes = GetZipBytes(); | ||
| if (zipBytes > 0) { | ||
| total += GetTotBytes(); | ||
| } | ||
| TBufferFile b(TBuffer::kWrite, 10000); | ||
| TTree::Class()->WriteBuffer(b, (TTree*) this); | ||
| total += b.Length(); | ||
| Long64_t file = fZipBytes + s; | ||
| Long64_t file = zipBytes + s; | ||
| Float_t cx = 1; | ||
| if (fZipBytes) { | ||
| cx = (fTotBytes + 0.00001) / fZipBytes; | ||
| if (zipBytes) { | ||
| cx = (GetTotBytes() + 0.00001) / zipBytes; | ||
| } | ||
| Printf("******************************************************************************"); | ||
| Printf("*Tree :%-10s: %-54s *", GetName(), GetTitle()); | ||
|
|
@@ -7352,8 +7397,11 @@ void TTree::Refresh() | |
|
|
||
| fAutoSave = tree->fAutoSave; | ||
| fEntries = tree->fEntries; | ||
| fTotBytes = tree->fTotBytes; | ||
| fZipBytes = tree->fZipBytes; | ||
| { | ||
| std::lock_guard<std::mutex> sentry(fCounterMutex); | ||
| fTotBytes = tree->GetTotBytes(); | ||
| fZipBytes = tree->GetZipBytes(); | ||
| } | ||
| fSavedBytes = tree->fSavedBytes; | ||
| fTotalBuffers = tree->fTotalBuffers.load(); | ||
|
|
||
|
|
@@ -7404,8 +7452,11 @@ void TTree::Reset(Option_t* option) | |
| fNotify = 0; | ||
| fEntries = 0; | ||
| fNClusterRange = 0; | ||
| fTotBytes = 0; | ||
| fZipBytes = 0; | ||
| { | ||
| std::lock_guard<std::mutex> sentry(fCounterMutex); | ||
| fTotBytes = 0; | ||
| fZipBytes = 0; | ||
| } | ||
| fFlushedBytes = 0; | ||
| fSavedBytes = 0; | ||
| fTotalBuffers = 0; | ||
|
|
@@ -7434,8 +7485,11 @@ void TTree::ResetAfterMerge(TFileMergeInfo *info) | |
| { | ||
| fEntries = 0; | ||
| fNClusterRange = 0; | ||
| fTotBytes = 0; | ||
| fZipBytes = 0; | ||
| { | ||
| std::lock_guard<std::mutex> sentry(fCounterMutex); | ||
| fTotBytes = 0; | ||
| fZipBytes = 0; | ||
| } | ||
| fSavedBytes = 0; | ||
| fFlushedBytes = 0; | ||
| fTotalBuffers = 0; | ||
|
|
@@ -8823,10 +8877,12 @@ void TTree::Streamer(TBuffer& b) | |
| } else if (fAutoFlush != 0) { | ||
| // Estimate the cluster size. | ||
| // This will allow TTree::Process to enable the cache. | ||
| if (fZipBytes != 0) { | ||
| fCacheSize = fAutoFlush*(fZipBytes/fEntries); | ||
| } else if (fTotBytes != 0) { | ||
| fCacheSize = fAutoFlush*(fTotBytes/fEntries); | ||
| Long64_t zipBytes = GetZipBytes(); | ||
| Long64_t totBytes = GetTotBytes(); | ||
| if (zipBytes != 0) { | ||
| fCacheSize = fAutoFlush*(zipBytes/fEntries); | ||
| } else if (totBytes != 0) { | ||
| fCacheSize = fAutoFlush*(totBytes/fEntries); | ||
| } else { | ||
| fCacheSize = 30000000; | ||
| } | ||
|
|
@@ -8852,16 +8908,22 @@ void TTree::Streamer(TBuffer& b) | |
| b >> ijunk; fMaxEntryLoop = (Long64_t)ijunk; | ||
| b >> ijunk; fMaxVirtualSize = (Long64_t)ijunk; | ||
| b >> djunk; fEntries = (Long64_t)djunk; | ||
| b >> djunk; fTotBytes = (Long64_t)djunk; | ||
| b >> djunk; fZipBytes = (Long64_t)djunk; | ||
| { | ||
| std::lock_guard<std::mutex> sentry(fCounterMutex); | ||
| b >> djunk; fTotBytes = (Long64_t)djunk; | ||
| b >> djunk; fZipBytes = (Long64_t)djunk; | ||
| } | ||
| b >> ijunk; fAutoSave = (Long64_t)ijunk; | ||
| b >> ijunk; fEstimate = (Long64_t)ijunk; | ||
| if (fEstimate <= 10000) fEstimate = 1000000; | ||
| fBranches.Streamer(b); | ||
| if (fBranchRef) fBranchRef->SetTree(this); | ||
| TBranch__SetTree(this,fBranches); | ||
| fLeaves.Streamer(b); | ||
| fSavedBytes = fTotBytes; | ||
| { | ||
| std::lock_guard<std::mutex> sentry(fCounterMutex); | ||
| fSavedBytes = fTotBytes; | ||
| } | ||
| if (R__v > 1) fIndexValues.Streamer(b); | ||
| if (R__v > 2) fIndex.Streamer(b); | ||
| if (R__v > 3) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see
#include <mutex>?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.