Skip to content

Commit fd8b95b

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 dd9c2d3 commit fd8b95b

4 files changed

Lines changed: 209 additions & 31 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: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,27 +1496,59 @@ Bool_t TFile::IsOpen() const
14961496

14971497
void TFile::MakeFree(Long64_t first, Long64_t last)
14981498
{
1499-
TFree *f1 = (TFree*)fFree->First();
1500-
if (!f1) return;
1501-
TFree *newfree = f1->AddFree(fFree,first,last);
1502-
if(!newfree) return;
1499+
assert(first > 0 && last > first && last < fEND);
1500+
1501+
TFree *f1 = static_cast<TFree *>(fFree->First());
1502+
assert(f1); // There must always be at least the virtual free segment at the end of the file
1503+
1504+
TFree *newfree = f1->AddFree(fFree, first, last);
1505+
assert(newfree); // AddFree always succeeds
1506+
15031507
Long64_t nfirst = newfree->GetFirst();
15041508
Long64_t nlast = newfree->GetLast();
1505-
Long64_t nbytesl= nlast-nfirst+1;
1506-
if (nbytesl > 2000000000) nbytesl = 2000000000;
1507-
Int_t nbytes = -Int_t (nbytesl);
1508-
char buffer[sizeof(Int_t)];
1509-
char *pbuffer = buffer;
1510-
tobuf(pbuffer, nbytes);
1511-
if (last == fEND-1) fEND = nfirst;
1512-
Seek(nfirst);
1513-
// We could not update the meta data for this block on the file.
1514-
// This is not fatal as this only means that we won't get it 'right'
1515-
// if we ever need to Recover the file before the block is actually
1516-
// (attempted to be reused.
1517-
// coverity[unchecked_value]
1518-
WriteBuffer(buffer, sizeof(buffer));
1519-
if (fMustFlush) Flush();
1509+
1510+
// The new free segment may close a series of consecutive free segments at the end of the file.
1511+
// These segments need to be merged so that the last free segment is [fEND + 1 .. max file size].
1512+
auto tail = static_cast<TFree *>(fFree->Last());
1513+
while (tail != fFree->First()) {
1514+
// Starting from the last segment, merge in previous segments if they are adjacent
1515+
auto prev = (TFree *)fFree->Before(tail);
1516+
if (prev->GetLast() + 1 < tail->GetFirst())
1517+
break;
1518+
assert(prev->GetLast() + 1 == tail->GetFirst());
1519+
tail->SetFirst(prev->GetFirst());
1520+
fFree->Remove(prev);
1521+
delete prev;
1522+
}
1523+
if (tail->GetFirst() <= nfirst) {
1524+
nfirst = tail->GetFirst();
1525+
nlast = tail->GetLast();
1526+
}
1527+
1528+
// 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
1529+
// to write the free segment size past the new end of the file (besides, the length of this "virtual" free
1530+
// segment may anyway be larger than TFree::kMaxGapSize).
1531+
if (last == fEND - 1) {
1532+
fEND = nfirst;
1533+
} else {
1534+
Long64_t nbytesl = nlast - nfirst + 1;
1535+
assert(nbytesl <= TFree::kMaxGapSize);
1536+
1537+
Int_t nbytes = -static_cast<Int_t>(nbytesl);
1538+
char buffer[sizeof(Int_t)];
1539+
char *pbuffer = buffer;
1540+
tobuf(pbuffer, nbytes);
1541+
1542+
Seek(nfirst);
1543+
// We could not update the meta data for this block on the file.
1544+
// This is not fatal as this only means that we won't get it 'right'
1545+
// if we ever need to Recover the file before the block is actually
1546+
// (attempted to be reused.
1547+
// coverity[unchecked_value]
1548+
WriteBuffer(buffer, sizeof(buffer));
1549+
if (fMustFlush)
1550+
Flush();
1551+
}
15201552
}
15211553

15221554
////////////////////////////////////////////////////////////////////////////////

io/io/src/TFree.cxx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,36 @@ TFree::TFree(TList *lfree, Long64_t first, Long64_t last)
6161
/// - if first just follows an existing free segment AND last just precedes
6262
/// an existing free segment, these two segments are merged into
6363
/// one single segment.
64+
/// - Do not join free segments, however, if the combined resulting segment
65+
/// would be larger than 2GB.
6466
///
6567

6668
TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
6769
{
70+
assert(first < last);
71+
6872
TFree *idcur = this;
69-
while (idcur) {
73+
do {
7074
Long64_t curfirst = idcur->GetFirst();
7175
Long64_t curlast = idcur->GetLast();
72-
if (curlast == first-1) {
76+
77+
if ((curlast == first - 1) && (last - curfirst < kMaxGapSize)) {
7378
idcur->SetLast(last);
74-
TFree *idnext = (TFree*)lfree->After(idcur);
75-
if (idnext == 0) return idcur;
76-
if (idnext->GetFirst() > last+1) return idcur;
77-
idcur->SetLast( idnext->GetLast() );
79+
// Merged new segment with previous one; is there a next segment?
80+
TFree *idnext = static_cast<TFree *>(lfree->After(idcur));
81+
if (idnext == nullptr)
82+
return idcur;
83+
84+
// Continue only if the next segment is adjacent to the newly merged one (and not too big)
85+
if ((idnext->GetFirst() > last + 1) || (idnext->GetLast() - curfirst >= kMaxGapSize))
86+
return idcur;
87+
88+
idcur->SetLast(idnext->GetLast());
7889
lfree->Remove(idnext);
7990
delete idnext;
8091
return idcur;
8192
}
82-
if (curfirst == last+1) {
93+
if ((curfirst == last + 1) && (curlast - first < kMaxGapSize)) {
8394
idcur->SetFirst(first);
8495
return idcur;
8596
}
@@ -91,8 +102,9 @@ TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
91102
return newfree;
92103
}
93104
idcur = (TFree*)lfree->After(idcur);
94-
}
95-
return 0;
105+
} while (idcur);
106+
107+
return nullptr;
96108
}
97109

98110
////////////////////////////////////////////////////////////////////////////////
@@ -186,4 +198,3 @@ Int_t TFree::Sizeof() const
186198
if (fLast > TFile::kStartBigFile) return 18;
187199
else return 10;
188200
}
189-

io/io/test/TFileTests.cxx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include "TSystem.h"
1818
#include "TEnv.h" // gEnv
1919

20+
#include <cstdio>
21+
2022
TEST(TFile, WriteObjectTObject)
2123
{
2224
auto filename{"tfile_writeobject_tobject.root"};
@@ -332,3 +334,134 @@ TEST(TFile, UUID)
332334
TMemFile f("uuidtest.root", "RECREATE");
333335
EXPECT_EQ('4', f.GetUUID().AsString()[14]);
334336
}
337+
338+
TEST(TFile, DeleteKey)
339+
{
340+
struct FileRaii {
341+
std::string fFilename;
342+
FileRaii(std::string_view fname) : fFilename(fname) {}
343+
~FileRaii() { gSystem->Unlink(fFilename.c_str()); }
344+
} fileGuard("tfile_test_delete_keys.root");
345+
346+
auto fnCountGaps = [](const std::string &fileName) {
347+
auto f = std::unique_ptr<TFile>(TFile::Open(fileName.c_str()));
348+
std::uint64_t nGaps = 0;
349+
for (const auto &k : f->WalkTKeys()) {
350+
if (k.fType == ROOT::Detail::TKeyMapNode::kGap)
351+
nGaps++;
352+
}
353+
return nGaps;
354+
};
355+
356+
auto f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "RECREATE"));
357+
f->SetCompressionSettings(0);
358+
f->Write();
359+
f->Close();
360+
361+
// The empty file should have no gaps. Note that gaps are created temporarily when certain keys are overwritten.
362+
EXPECT_EQ(0, fnCountGaps(fileGuard.fFilename));
363+
364+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
365+
std::vector<char> v;
366+
f->WriteObject(&v, "va0");
367+
f->WriteObject(&v, "va1");
368+
f->WriteObject(&v, "va2");
369+
f->Write();
370+
f->Close();
371+
// 2 gaps: new (larger) keys list and free list are written
372+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
373+
374+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
375+
f->Delete("va1;*"); // should create small gap that cannot be merged, trapped between v0 and v2
376+
f->Write();
377+
f->Close();
378+
379+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
380+
381+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
382+
f->Delete("va2;*"); // gaps at the tail should merge
383+
f->Write();
384+
f->Close();
385+
386+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
387+
388+
// The following tests run out of memory on 32bit platforms
389+
if (sizeof(std::size_t) == 4) {
390+
printf("Skipping test partially on 32bit platform.\n");
391+
return;
392+
}
393+
394+
v.resize(1000 * 1000 * 1000 - 100, 'x'); // almost 1GB
395+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
396+
f->WriteObject(&v, "vb0");
397+
f->WriteObject(&v, "vb1");
398+
v.resize(1000 * 1000); // truncate next objects to 1MB
399+
f->WriteObject(&v, "vc0");
400+
f->WriteObject(&v, "vc1");
401+
f->WriteObject(&v, "vc2");
402+
f->WriteObject(&v, "vc3");
403+
f->Write();
404+
EXPECT_GT(f->GetEND(), TFile::kStartBigFile);
405+
f->Close();
406+
407+
// New keys list, hence 3 gaps
408+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
409+
410+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
411+
f->Delete("vb0;*"); // |
412+
f->Delete("vb1;*"); // |---> First merged gap
413+
f->Delete("vc0;*"); // |
414+
f->Delete("vc1;*"); // |---> Second merged gap
415+
f->Write();
416+
f->Close();
417+
418+
// Two merged gaps, the new keys list fits into either of them
419+
EXPECT_EQ(4, fnCountGaps(fileGuard.fFilename));
420+
421+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
422+
// Delete the remaining data at the tail of the file in reverse order
423+
f->Delete("vc2;*");
424+
f->Delete("vc3;*");
425+
f->Write();
426+
// Back to small file
427+
EXPECT_LT(f->GetEND(), TFile::kStartBigFile);
428+
f->Close();
429+
430+
// Only the original 2 gaps from the first keys list and free list overwrite
431+
EXPECT_EQ(2, fnCountGaps(fileGuard.fFilename));
432+
433+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
434+
v.resize(700 * 1000 * 1000); // construct objects such that 3 consecutive gaps surpass 2GB (but not 2)
435+
f->WriteObject(&v, "vd0");
436+
f->WriteObject(&v, "vd1");
437+
f->WriteObject(&v, "vd2");
438+
f->WriteObject(&v, "vd3");
439+
f->WriteObject(&v, "vd4");
440+
f->Write();
441+
f->Close();
442+
443+
// New keys list --> 3 gaps
444+
EXPECT_EQ(3, fnCountGaps(fileGuard.fFilename));
445+
446+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
447+
f->Delete("vd1;*");
448+
f->Delete("vd3;*");
449+
f->Write();
450+
f->Close();
451+
452+
// Nothing mergable, 2 more gaps
453+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
454+
455+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str(), "UPDATE"));
456+
auto theEnd = f->GetEND();
457+
f->Delete("vd2;*");
458+
f->Write();
459+
f->Close();
460+
461+
// We can only merge the gaps of v1 and v2, not all three (vd1, vd2, vd3) due to the gap size
462+
EXPECT_EQ(5, fnCountGaps(fileGuard.fFilename));
463+
464+
// Ensure that the file's tail is still intact
465+
f = std::unique_ptr<TFile>(TFile::Open(fileGuard.fFilename.c_str()));
466+
EXPECT_EQ(f->GetEND(), theEnd);
467+
}

0 commit comments

Comments
 (0)