Skip to content

Commit bffb03a

Browse files
committed
DPL Analysis: cap the read buffers of the native AOD reader
The native reader fetches the compressed baskets of all the trees a DF is read from with one coalesced read into one buffer, kept until the DF is done, and the buffer pool keeps up to two of them for reuse. On Run 2 AO2Ds (about 62 MB of compressed data per DF) this holds about 75 MB of private memory per reader for the whole job: IgProf shows the largest allocation of the reader, 72 MB, in ReadBufferPool::get, and smaps shows it as anonymous memory, unaffected by fewer threads, no prefetch or MALLOC_ARENA_MAX=1. --aod-reader-buffer-mb N caps that. The trees of a DF, in the order they are read, are packed into read plans of at most N MB of compressed data; a plan is freed when the reads of its trees are done, and the buffer pool keeps at most N MB of free buffers. A DF which fits is still read in one go, so small DFs are not affected, and a single tree larger than N gets a plan of its own. The lookahead for remote files prepares only the trees which fit. Only the trees the reader asks for are read, as before. The default is 0, no limit, which keeps the current behaviour until the default is chosen from measurements.
1 parent 86084e0 commit bffb03a

9 files changed

Lines changed: 113 additions & 12 deletions

File tree

‎Framework/AODNative/include/AODNative/ByteSource.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ struct Options {
3636
int threads = 1;
3737
int prefetchDepth = 1;
3838
size_t coalesceGap = 64 * 1024;
39+
/// Most bytes the read plans of a file hold at once, 0 for no limit. A DF
40+
/// larger than this is read in groups of trees.
41+
size_t bufferBudget = 0;
3942
double spanFraction = 0.8;
4043
int streamWindows = 4;
4144
size_t streamWindowBytes = 2 * 1024 * 1024;

‎Framework/AODNative/include/AODNative/ReadPlan.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ class ReadBufferPool
3232
std::unique_ptr<uint8_t[]> data;
3333
size_t capacity = 0;
3434
};
35+
/// Keeps at most @a maxFreeBytes in free buffers, dropping the largest
36+
/// first; with 0, at most kMaxFree buffers, dropping the smallest first.
37+
explicit ReadBufferPool(size_t maxFreeBytes = 0) : mMaxFreeBytes(maxFreeBytes) {}
3538
/// The smallest free buffer of at least @a size bytes, or a new one.
3639
Buffer get(size_t size);
3740
void put(Buffer buffer);
3841

3942
private:
4043
static constexpr size_t kMaxFree = 2;
44+
size_t mMaxFreeBytes;
4145
std::mutex mMutex;
4246
std::vector<Buffer> mFree;
4347
};
@@ -58,6 +62,8 @@ class ReadPlan
5862
void add(uint64_t offset, size_t length);
5963
/// Registers every basket of @a tree which is not embedded in its TBranch.
6064
void add(TreeInfo const& tree);
65+
/// The bytes add(@a tree) registers.
66+
static uint64_t bytesOf(TreeInfo const& tree);
6167
/// The bytes of [offset, offset + length), if a registered range covers
6268
/// them, fetching the pending ranges first if needed. Empty otherwise. The
6369
/// span stays valid as long as the plan.

‎Framework/AODNative/src/ReadPlan.cxx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,21 @@ void ReadBufferPool::put(Buffer buffer)
4242
{
4343
std::lock_guard lock{mMutex};
4444
mFree.push_back(std::move(buffer));
45-
if (mFree.size() > kMaxFree) {
45+
if (mMaxFreeBytes == 0 && mFree.size() > kMaxFree) {
4646
mFree.erase(std::ranges::min_element(mFree, {}, &Buffer::capacity));
4747
}
48+
auto freeBytes = [this] {
49+
size_t total = 0;
50+
for (auto const& b : mFree) {
51+
total += b.capacity;
52+
}
53+
return total;
54+
};
55+
// The same headroom as get() allocates, so that a buffer for a group just
56+
// under the budget can still be kept.
57+
while (mMaxFreeBytes > 0 && !mFree.empty() && freeBytes() > mMaxFreeBytes + mMaxFreeBytes / 8) {
58+
mFree.erase(std::ranges::max_element(mFree, {}, &Buffer::capacity));
59+
}
4860
}
4961

5062
ReadPlan::ReadPlan(std::shared_ptr<ByteSource> source, Options const& options, std::shared_ptr<ReadBufferPool> pool)
@@ -80,6 +92,19 @@ void ReadPlan::add(TreeInfo const& tree)
8092
}
8193
}
8294

95+
uint64_t ReadPlan::bytesOf(TreeInfo const& tree)
96+
{
97+
uint64_t total = 0;
98+
for (auto const& branch : tree.branches) {
99+
for (auto const& basket : branch.baskets) {
100+
if (!basket.embedded) {
101+
total += basket.nbytes;
102+
}
103+
}
104+
}
105+
return total;
106+
}
107+
83108
std::span<const uint8_t> ReadPlan::lookup(uint64_t offset, size_t length) const
84109
{
85110
auto it = std::upper_bound(mChunks.begin(), mChunks.end(), offset, [](uint64_t off, Chunk const& c) { return off < c.offset; });

‎Framework/AODNative/test/test_ReadPlan.cxx‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,27 @@ TEST_CASE("buffers of finished plans are reused by the next one")
185185
auto big = pool->get(1 << 16);
186186
REQUIRE(big.capacity >= 1 << 16);
187187
}
188+
189+
TEST_CASE("a pool with a budget drops the largest free buffers beyond it")
190+
{
191+
ReadBufferPool pool{1000};
192+
auto small = pool.get(400);
193+
auto medium = pool.get(500);
194+
auto large = pool.get(2000);
195+
auto* smallData = small.data.get();
196+
auto* mediumData = medium.data.get();
197+
pool.put(std::move(small));
198+
pool.put(std::move(medium));
199+
pool.put(std::move(large)); // over the budget: the largest goes
200+
REQUIRE(pool.get(300).data.get() == smallData);
201+
REQUIRE(pool.get(480).data.get() == mediumData);
202+
}
203+
204+
TEST_CASE("bytesOf counts the baskets a plan reads, not the embedded ones")
205+
{
206+
TreeInfo tree;
207+
tree.branches.resize(2);
208+
tree.branches[0].baskets = {BasketRef{100, 300, 0, 10, false, {}}, BasketRef{400, 200, 10, 5, false, {}}};
209+
tree.branches[1].baskets = {BasketRef{600, 50, 0, 10, false, {}}, BasketRef{0, 0, 10, 5, true, {1, 2, 3}}};
210+
REQUIRE(ReadPlan::bytesOf(tree) == 550);
211+
}

‎Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
206206
}
207207
context.nativeOptions.threads = std::max(1, options.get<int>("aod-reader-threads"));
208208
context.nativeOptions.prefetchDepth = std::max(0, options.get<int>("aod-reader-prefetch"));
209+
context.nativeOptions.bufferBudget = std::max(0, options.get<int>("aod-reader-buffer-mb")) * size_t{1024 * 1024};
209210
auto localIO = options.get<std::string>("aod-reader-local-io");
210211
if (localIO != "mmap" && localIO != "pread") {
211212
LOGP(fatal, "Invalid --aod-reader-local-io \"{}\": use mmap or pread.", localIO);

‎Framework/AnalysisSupport/src/NativeAOD.cxx‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace o2::framework
2121
using namespace o2::aod::native;
2222

2323
NativeAODFileSystem::NativeAODFileSystem(std::shared_ptr<RootFile> file, Options options, RootObjectReadingFactory& factory)
24-
: mFile(std::move(file)), mOptions(options), mObjectFactory(factory)
24+
: mFile(std::move(file)), mOptions(options), mObjectFactory(factory), mPool(std::make_shared<ReadBufferPool>(options.bufferBudget))
2525
{
2626
}
2727

@@ -63,12 +63,20 @@ std::shared_ptr<ReadPlan> NativeAODFileSystem::planFor(std::string const& path)
6363
if (df != mPlanDF) {
6464
auto previous = std::move(mPlanTrees);
6565
mPlanTrees.clear();
66+
mPlanOf.clear();
67+
mPlanBytes = 0;
6668
mPlanDF = df;
6769
// Also when the directory is already known, e.g. the first DF, read at open.
6870
adoptLookahead(df);
6971
directory(df);
7072
if (mAdoptedDF == df) {
7173
mPlan = std::move(mAdoptedPlan);
74+
for (auto& name : mAdoptedTrees) {
75+
mPlanOf[name] = mPlan;
76+
if (auto* handle = findTree(df, name)) {
77+
mPlanBytes += ReadPlan::bytesOf(handle->tree);
78+
}
79+
}
7280
mPlanTrees = std::move(mAdoptedTrees);
7381
} else {
7482
mPlan = std::make_shared<ReadPlan>(mFile->sourcePtr(), mOptions, mPool);
@@ -90,7 +98,15 @@ std::shared_ptr<ReadPlan> NativeAODFileSystem::planFor(std::string const& path)
9098
}
9199
}
92100
addToPlan(treeName);
93-
return mPlan;
101+
// Let go of the plan once its tree is handed out: the plan, and its buffers,
102+
// then go away when the reads of its trees are done.
103+
auto it = mPlanOf.find(treeName);
104+
if (it == mPlanOf.end()) {
105+
return nullptr;
106+
}
107+
auto plan = std::move(it->second);
108+
mPlanOf.erase(it);
109+
return plan;
94110
}
95111

96112
void NativeAODFileSystem::releaseMappedDF(std::string const& df)
@@ -182,10 +198,19 @@ void NativeAODFileSystem::startLookahead(std::string const& afterDF, std::vector
182198
Lookahead lookahead{df, {}, {}, std::make_shared<ReadPlan>(file->sourcePtr(), options, pool)};
183199
file->setTreesToPrefetch(trees);
184200
lookahead.keys = file->keysOf(df);
201+
// Only the trees which fit in the budget; the others are read when asked for.
202+
uint64_t bytes = 0;
185203
for (auto const& name : trees) {
186204
for (auto const& key : lookahead.keys) {
187205
if (key.name == name && key.className == "TTree") {
188-
lookahead.trees.emplace_back(name, file->readTree(key));
206+
auto tree = file->readTree(key);
207+
auto treeBytes = ReadPlan::bytesOf(tree);
208+
if (options.bufferBudget > 0 && bytes > 0 && bytes + treeBytes > options.bufferBudget) {
209+
lookahead.plan->fetch();
210+
return lookahead;
211+
}
212+
bytes += treeBytes;
213+
lookahead.trees.emplace_back(name, std::move(tree));
189214
lookahead.plan->add(lookahead.trees.back().second);
190215
}
191216
}
@@ -210,7 +235,14 @@ void NativeAODFileSystem::addToPlan(std::string const& treeName)
210235
// Reported when the tree itself is asked for.
211236
}
212237
if (handle) {
238+
auto bytes = ReadPlan::bytesOf(handle->tree);
239+
if (mOptions.bufferBudget > 0 && mPlanBytes > 0 && mPlanBytes + bytes > mOptions.bufferBudget) {
240+
mPlan = std::make_shared<ReadPlan>(mFile->sourcePtr(), mOptions, mPool);
241+
mPlanBytes = 0;
242+
}
213243
mPlan->add(handle->tree);
244+
mPlanBytes += bytes;
245+
mPlanOf[treeName] = mPlan;
214246
mPlanTrees.push_back(treeName);
215247
}
216248
}

‎Framework/AnalysisSupport/src/NativeAOD.h‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,17 @@ class NativeAODFileSystem : public VirtualRootFileSystemBase
5252
/// The tree at @a path, parsed on first use. nullptr if there is no such
5353
/// tree. Throws o2::aod::native::FormatError if it cannot be parsed.
5454
NativeTreeHandle* getTreeHandle(std::string const& path);
55-
/// The read plan of the DF of the tree at @a path, with that tree in it;
56-
/// nullptr when the file is mapped and nothing needs reading. A new DF gets a
57-
/// new plan, with the trees read in the previous DF already in it: a reader
58-
/// asks for the same tables in every DF, so the first read of a DF fetches
59-
/// all of them together. For a remote file, the next DF is prepared in the
60-
/// background meanwhile (keys, TTrees and data of the same trees), so that
61-
/// its round trips overlap with the decoding of this one.
55+
/// The read plan with the tree at @a path in it; nullptr when the file is
56+
/// mapped and nothing needs reading. A new DF gets new plans, with the trees
57+
/// read in the previous DF already in them: a reader asks for the same tables
58+
/// in every DF, so the first read of a plan fetches all of its trees
59+
/// together. With Options::bufferBudget, the trees of a DF are split into
60+
/// plans of at most that many bytes, in the order they are read, and a plan
61+
/// is freed once the reads of its trees are done; a tree larger than the
62+
/// budget gets a plan of its own. For a remote file, the next DF is prepared
63+
/// in the background meanwhile (keys, TTrees and data of the same trees, up
64+
/// to the budget), so that its round trips overlap with the decoding of this
65+
/// one.
6266
std::shared_ptr<o2::aod::native::ReadPlan> planFor(std::string const& path);
6367
o2::aod::native::RootFile& file() { return *mFile; }
6468
/// The name the file was asked for by (an LFN for a grid file), which is
@@ -97,8 +101,12 @@ class NativeAODFileSystem : public VirtualRootFileSystemBase
97101
std::mutex mMutex;
98102
std::map<std::string, std::vector<o2::aod::native::KeyInfo>> mDirectories;
99103
std::map<std::string, std::unique_ptr<NativeTreeHandle>> mTrees;
100-
std::shared_ptr<o2::aod::native::ReadBufferPool> mPool = std::make_shared<o2::aod::native::ReadBufferPool>();
104+
std::shared_ptr<o2::aod::native::ReadBufferPool> mPool;
105+
// The plan trees are being added to, and how many bytes it holds.
101106
std::shared_ptr<o2::aod::native::ReadPlan> mPlan;
107+
uint64_t mPlanBytes = 0;
108+
// The plan of each tree of the DF, until planFor() hands it out.
109+
std::map<std::string, std::shared_ptr<o2::aod::native::ReadPlan>> mPlanOf;
102110
std::string mPlanDF;
103111
// The DF whose pages are mapped, released when the next one starts.
104112
std::string mMappedDF;

‎Framework/Core/src/WorkflowHelpers.cxx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ void WorkflowHelpers::injectServiceDevices(WorkflowSpec& workflow, ConfigContext
227227
ConfigParamSpec{"aod-reader-threads", VariantType::Int, 1, {"Decompression threads of the native AOD reader"}},
228228
ConfigParamSpec{"aod-reader-prefetch", VariantType::Int, 1, {"DFs the native AOD reader prepares ahead for remote files: 0 (off) or 1"}},
229229
ConfigParamSpec{"aod-reader-local-io", VariantType::String, "pread", {"How the native AOD reader reads local files: pread or mmap"}},
230+
ConfigParamSpec{"aod-reader-buffer-mb", VariantType::Int, 0, {"Most MB the native AOD reader holds in read buffers, 0 for no limit"}},
230231
ConfigParamSpec{"aod-reader-json", VariantType::String, {"json configuration file"}},
231232
ConfigParamSpec{"time-limit", VariantType::Int64, 0ll, {"Maximum run time limit in seconds"}},
232233
ConfigParamSpec{"orbit-offset-enumeration", VariantType::Int64, 0ll, {"initial value for the orbit"}},

‎Framework/Core/src/runDataProcessing.cxx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,6 +2089,7 @@ int runStateMachine(DataProcessorSpecs const& workflow,
20892089
"--aod-reader-threads",
20902090
"--aod-reader-prefetch",
20912091
"--aod-reader-local-io",
2092+
"--aod-reader-buffer-mb",
20922093
"--aod-parent-access-level",
20932094
"--aod-parent-base-path-replacement",
20942095
"--aod-origin-level-mapping",

0 commit comments

Comments
 (0)