Skip to content

Commit 6449f14

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 0d2aa0e commit 6449f14

4 files changed

Lines changed: 178 additions & 16 deletions

File tree

io/io/inc/TFree.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525

2626

2727
class TFree : public TObject {
28-
2928
protected:
3029
Long64_t fFirst; ///<First free word of segment
3130
Long64_t fLast; ///<Last free word of segment
3231

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

io/io/src/TFile.cxx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,27 +1474,55 @@ 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+
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 = (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+
Long64_t nbytesl = nlast - nfirst + 1;
1506+
if (nbytesl > TFree::kMaxGapSize)
1507+
nbytesl = TFree::kMaxGapSize;
1508+
1509+
Int_t nbytes = -static_cast<Int_t>(nbytesl);
14861510
char buffer[sizeof(Int_t)];
14871511
char *pbuffer = buffer;
14881512
tobuf(pbuffer, nbytes);
1489-
if (last == fEND-1) fEND = nfirst;
1513+
1514+
// The file will get smaller if a free segment is added at the end.
1515+
if (last == fEND - 1)
1516+
fEND = nfirst;
14901517
Seek(nfirst);
14911518
// We could not update the meta data for this block on the file.
14921519
// This is not fatal as this only means that we won't get it 'right'
14931520
// if we ever need to Recover the file before the block is actually
14941521
// (attempted to be reused.
14951522
// coverity[unchecked_value]
14961523
WriteBuffer(buffer, sizeof(buffer));
1497-
if (fMustFlush) Flush();
1524+
if (fMustFlush)
1525+
Flush();
14981526
}
14991527

15001528
////////////////////////////////////////////////////////////////////////////////

io/io/src/TFree.cxx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,23 @@ TFree *TFree::AddFree(TList *lfree, Long64_t first, Long64_t last)
7070
while (idcur) {
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+
// Is the next segment 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
}

io/io/test/TFileTests.cxx

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

0 commit comments

Comments
 (0)