Skip to content

Commit 2e131df

Browse files
committed
Fix TFile gap coalescing
Prevent merged free segments from becoming bigger than 2GB. Requires additional code to ensure that gaps at the end of the file are always merged.
1 parent a3a6d5d commit 2e131df

4 files changed

Lines changed: 201 additions & 29 deletions

File tree

io/io/inc/TFree.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@
2323

2424
#include "TObject.h"
2525

26-
2726
class TFree : public TObject {
28-
2927
protected:
3028
Long64_t fFirst; ///<First free word of segment
3129
Long64_t fLast; ///<Last free word of segment
3230

3331
public:
32+
/// Prevent gap coalescing from exceeding 2 GB, since the gap size is stored as a signed 4 bytes integer.
33+
/// The true limit is 2 GiB but for historical reasons (previous implementation of TFree) we stick to the 2GB limit.
34+
static constexpr Int_t kMaxGapSize = 2000 * 1000 * 1000;
35+
3436
TFree();
3537
TFree(TList *lfree, Long64_t first, Long64_t last);
3638
~TFree() override;

io/io/src/TFile.cxx

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,27 +1474,58 @@ Bool_t TFile::IsOpen() const
14741474

14751475
void TFile::MakeFree(Long64_t first, Long64_t last)
14761476
{
1477-
TFree *f1 = (TFree*)fFree->First();
1478-
if (!f1) return;
1479-
TFree *newfree = f1->AddFree(fFree,first,last);
1480-
if(!newfree) return;
1477+
TFree *f1 = static_cast<TFree *>(fFree->First());
1478+
if (!f1)
1479+
return;
1480+
1481+
TFree *newfree = f1->AddFree(fFree, first, last);
1482+
if (!newfree)
1483+
return;
14811484
Long64_t nfirst = newfree->GetFirst();
14821485
Long64_t nlast = newfree->GetLast();
1483-
Long64_t nbytesl= nlast-nfirst+1;
1484-
if (nbytesl > 2000000000) nbytesl = 2000000000;
1485-
Int_t nbytes = -Int_t (nbytesl);
1486-
char buffer[sizeof(Int_t)];
1487-
char *pbuffer = buffer;
1488-
tobuf(pbuffer, nbytes);
1489-
if (last == fEND-1) fEND = nfirst;
1490-
Seek(nfirst);
1491-
// We could not update the meta data for this block on the file.
1492-
// This is not fatal as this only means that we won't get it 'right'
1493-
// if we ever need to Recover the file before the block is actually
1494-
// (attempted to be reused.
1495-
// coverity[unchecked_value]
1496-
WriteBuffer(buffer, sizeof(buffer));
1497-
if (fMustFlush) Flush();
1486+
1487+
// The new free segment may close a series of consecutive free segments at the end of the file.
1488+
// These segments need to be merged so that the last free segment is [fEND + 1 .. max file size].
1489+
auto tail = static_cast<TFree *>(fFree->Last());
1490+
while (tail != fFree->First()) {
1491+
// Starting from the last segment, merge in previous segments if they are adjacent
1492+
auto prev = (TFree *)fFree->Before(tail);
1493+
if (prev->GetLast() + 1 < tail->GetFirst())
1494+
break;
1495+
assert(prev->GetLast() + 1 == tail->GetFirst());
1496+
tail->SetFirst(prev->GetFirst());
1497+
fFree->Remove(prev);
1498+
delete prev;
1499+
}
1500+
if (tail->GetFirst() <= nfirst) {
1501+
nfirst = tail->GetFirst();
1502+
nlast = tail->GetLast();
1503+
}
1504+
1505+
// The file will get smaller if a free segment is added at the end. In this case, we are done and we don't need
1506+
// to write the free segment size past the new end of the file (besides, the length of this "virtual" free
1507+
// segment may anyway be larger than TFree::kMaxGapSize).
1508+
if (last == fEND - 1) {
1509+
fEND = nfirst;
1510+
} else {
1511+
Long64_t nbytesl = nlast - nfirst + 1;
1512+
assert(nbytesl <= TFree::kMaxGapSize);
1513+
1514+
Int_t nbytes = -static_cast<Int_t>(nbytesl);
1515+
char buffer[sizeof(Int_t)];
1516+
char *pbuffer = buffer;
1517+
tobuf(pbuffer, nbytes);
1518+
1519+
Seek(nfirst);
1520+
// We could not update the meta data for this block on the file.
1521+
// This is not fatal as this only means that we won't get it 'right'
1522+
// if we ever need to Recover the file before the block is actually
1523+
// (attempted to be reused.
1524+
// coverity[unchecked_value]
1525+
WriteBuffer(buffer, sizeof(buffer));
1526+
if (fMustFlush)
1527+
Flush();
1528+
}
14981529
}
14991530

15001531
////////////////////////////////////////////////////////////////////////////////

io/io/src/TFree.cxx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,26 @@ TFree::TFree(TList *lfree, Long64_t first, Long64_t last)
6767
TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
6868
{
6969
TFree *idcur = this;
70-
while (idcur) {
70+
do {
7171
Long64_t curfirst = idcur->GetFirst();
7272
Long64_t curlast = idcur->GetLast();
73-
if (curlast == first-1) {
73+
if ((curlast == first - 1) && (last - curfirst + 1 <= kMaxGapSize)) {
7474
idcur->SetLast(last);
75-
TFree *idnext = (TFree*)lfree->After(idcur);
76-
if (idnext == 0) return idcur;
77-
if (idnext->GetFirst() > last+1) return idcur;
78-
idcur->SetLast( idnext->GetLast() );
75+
// Merged new segment with previous one; is there a next segment?
76+
TFree *idnext = static_cast<TFree *>(lfree->After(idcur));
77+
if (idnext == 0)
78+
return idcur;
79+
80+
// Continue only if the next segment is adjacent to the newly merged one (and not too big)
81+
if ((idnext->GetFirst() > last + 1) || (idnext->GetLast() - curfirst + 1 > kMaxGapSize))
82+
return idcur;
83+
84+
idcur->SetLast(idnext->GetLast());
7985
lfree->Remove(idnext);
8086
delete idnext;
8187
return idcur;
8288
}
83-
if (curfirst == last+1) {
89+
if ((curfirst == last + 1) && (curlast - first + 1 <= kMaxGapSize)) {
8490
idcur->SetFirst(first);
8591
return idcur;
8692
}
@@ -92,7 +98,7 @@ TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
9298
return newfree;
9399
}
94100
idcur = (TFree*)lfree->After(idcur);
95-
}
101+
} while (idcur);
96102
return 0;
97103
}
98104

io/io/test/TFileTests.cxx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "TSystem.h"
1515
#include "TEnv.h" // gEnv
1616

17+
#include <cstdio>
18+
1719
TEST(TFile, WriteObjectTObject)
1820
{
1921
auto filename{"tfile_writeobject_tobject.root"};
@@ -251,3 +253,134 @@ TEST(TFile, WalkTKeys)
251253
EXPECT_EQ(it->fKeyName, kLongerKey);
252254
EXPECT_EQ(it->fClassName, "string");
253255
}
256+
257+
TEST(TFile, DeleteKey)
258+
{
259+
struct FileRaii {
260+
std::string fFilename;
261+
FileRaii(std::string_view fname) : fFilename(fname) {}
262+
~FileRaii() { gSystem->Unlink(fFilename.c_str()); }
263+
} fileGuard("tfile_test_delete_keys.root");
264+
265+
auto fnCountGaps = [](const std::string &fileName) {
266+
auto f = std::unique_ptr<TFile>(TFile::Open(fileName.c_str()));
267+
std::uint64_t nGaps = 0;
268+
for (const auto &k : f->WalkTKeys()) {
269+
if (k.fType == ROOT::Detail::TKeyMapNode::kGap)
270+
nGaps++;
271+
}
272+
return nGaps;
273+
};
274+
275+
auto f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "RECREATE"));
276+
f->SetCompressionSettings(0);
277+
f->Write();
278+
f->Close();
279+
280+
// The empty file should have no gaps. Note that gaps are created temporarily when certain keys are overwritten.
281+
EXPECT_EQ(0, fnCountGaps(fileGuard.fFilename));
282+
283+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
284+
std::vector<char> v;
285+
f->WriteObject(&v, "va0");
286+
f->WriteObject(&v, "va1");
287+
f->WriteObject(&v, "va2");
288+
f->Write();
289+
f->Close();
290+
// 2 gaps: new (larger) keys list and free list are written
291+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
292+
293+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
294+
f->Delete("va1;*"); // should create small gap that cannot be merged, trapped between v0 and v2
295+
f->Write();
296+
f->Close();
297+
298+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
299+
300+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
301+
f->Delete("va2;*"); // gaps at the tail should merge
302+
f->Write();
303+
f->Close();
304+
305+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
306+
307+
// The following tests run out of memory on 32bit platforms
308+
if (sizeof(std::size_t) == 4) {
309+
printf("Skipping test partially on 32bit platform.\n");
310+
return;
311+
}
312+
313+
v.resize(1000 * 1000 * 1000 - 100, 'x'); // almost 1GB
314+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
315+
f->WriteObject(&v, "vb0");
316+
f->WriteObject(&v, "vb1");
317+
v.resize(1000 * 1000); // truncate next objects to 1MB
318+
f->WriteObject(&v, "vc0");
319+
f->WriteObject(&v, "vc1");
320+
f->WriteObject(&v, "vc2");
321+
f->WriteObject(&v, "vc3");
322+
f->Write();
323+
EXPECT_GT(f->GetEND(), TFile::kStartBigFile);
324+
f->Close();
325+
326+
// New keys list, hence 3 gaps
327+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
328+
329+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
330+
f->Delete("vb0;*"); // |
331+
f->Delete("vb1;*"); // |---> First merged gap
332+
f->Delete("vc0;*"); // |
333+
f->Delete("vc1;*"); // |---> Second merged gap
334+
f->Write();
335+
f->Close();
336+
337+
// Two merged gaps, the new keys list fits into either of them
338+
EXPECT_EQ(4, fnCountGaps(fileGuard.fFilename));
339+
340+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
341+
// Delete the remaining data at the tail of the file in reverse order
342+
f->Delete("vc2;*");
343+
f->Delete("vc3;*");
344+
f->Write();
345+
// Back to small file
346+
EXPECT_LT(f->GetEND(), TFile::kStartBigFile);
347+
f->Close();
348+
349+
// Only the original 2 gaps from the first keys list and free list overwrite
350+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
351+
352+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
353+
v.resize(700 * 1000 * 1000); // construct objects such that 3 consecutive gaps surpass 2GB (but not 2)
354+
f->WriteObject(&v, "vd0");
355+
f->WriteObject(&v, "vd1");
356+
f->WriteObject(&v, "vd2");
357+
f->WriteObject(&v, "vd3");
358+
f->WriteObject(&v, "vd4");
359+
f->Write();
360+
f->Close();
361+
362+
// New keys list --> 3 gaps
363+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
364+
365+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
366+
f->Delete("vd1;*");
367+
f->Delete("vd3;*");
368+
f->Write();
369+
f->Close();
370+
371+
// Nothing mergable, 2 more gaps
372+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
373+
374+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
375+
auto theEnd = f->GetEND();
376+
f->Delete("vd2;*");
377+
f->Write();
378+
f->Close();
379+
380+
// We can only merge the gaps of v1 and v2, not all three (vd1, vd2, vd3) due to the gap size
381+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
382+
383+
// Ensure that the file's tail is still intact
384+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str()));
385+
EXPECT_EQ(f->GetEND(), theEnd);
386+
}

0 commit comments

Comments
 (0)