Coverage Report

Created: 2026-10-20 19:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/storage/index/index_disk_usage.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "storage/index/index_disk_usage.h"
19
20
#include <algorithm>
21
#include <charconv>
22
#include <deque>
23
#include <filesystem>
24
#include <limits>
25
#include <map>
26
#include <memory>
27
#include <optional>
28
#include <set>
29
#include <system_error>
30
#include <tuple>
31
#include <utility>
32
33
#include "common/cast_set.h"
34
#include "common/check.h"
35
#include "io/io_common.h"
36
#include "storage/index/ann/ann_index_files.h"
37
#include "storage/index/index_file_reader.h"
38
#include "storage/index/inverted/common/inverted_index_desc.h"
39
#include "storage/index/inverted/snii/format/dict_entry.h"
40
#include "storage/index/inverted/snii/format/format_constants.h"
41
#include "storage/index/inverted/snii/reader/logical_index_reader.h"
42
#include "storage/olap_common.h"
43
#include "storage/rowset/beta_rowset.h"
44
#include "storage/rowset/rowset.h"
45
#include "storage/rowset/rowset_meta.h"
46
#include "storage/segment/segment.h"
47
48
namespace doris::segment_v2 {
49
50
namespace {
51
52
304
bool is_bkd_file(std::string_view name) {
53
304
    return name == InvertedIndexDescriptor::get_temporary_bkd_index_data_file_name() ||
54
304
           name == InvertedIndexDescriptor::get_temporary_bkd_index_meta_file_name() ||
55
304
           name == InvertedIndexDescriptor::get_temporary_bkd_index_file_name();
56
304
}
57
58
289
bool is_ann_file(std::string_view name) {
59
289
    return name == faiss_index_fila_name || name == faiss_ivfdata_file_name;
60
289
}
61
62
45
bool is_wanted(const IndexDiskUsageOptions& options, int64_t index_id) {
63
45
    return options.index_ids.empty() || options.index_ids.contains(index_id);
64
45
}
65
66
41
Status check_cancelled(const IndexDiskUsageOptions& options) {
67
41
    return options.check_cancelled ? options.check_cancelled() : Status::OK();
68
41
}
69
70
// A segment may have no index file on purpose, for example when every ANN index skipped a segment
71
// too small to train, or when a legacy table skipped writing indexes on load. Such a file holds
72
// no index bytes, while any other error is still reported.
73
24
bool is_absent_index_file(const Status& status) {
74
24
    return status.is<ErrorCode::INVERTED_INDEX_FILE_NOT_FOUND>() ||
75
24
           status.is<ErrorCode::INVERTED_INDEX_BYPASS>() || status.is<ErrorCode::NOT_FOUND>();
76
24
}
77
78
// Whether `status` says that the segment has no index container, which is only expected when the
79
// rowset meta records no container size: the size is recorded after the container is written.
80
8
bool is_skipped_container(const Status& status, const InvertedIndexFileInfo& file_info) {
81
8
    const bool recorded = file_info.has_index_size() && file_info.index_size() > 0;
82
8
    return !recorded && is_absent_index_file(status);
83
8
}
84
85
// A V1 index file and its size persisted in the rowset meta, or -1 when the size is not recorded.
86
struct V1IndexFile {
87
    const TabletIndex* index;
88
    int64_t persisted_size;
89
};
90
91
// Lists the V1 index files of a segment from the rowset meta, which records every written file,
92
// including the extracted VARIANT paths that the schema does not list. Rowsets without that record
93
// fall back to the inverted and ANN indexes of their own schema.
94
// `owned` keeps the indexes built from the rowset meta.
95
std::vector<V1IndexFile> list_v1_index_files(const TabletSchema& schema,
96
                                             const InvertedIndexFileInfo& file_info,
97
15
                                             std::deque<TabletIndex>* owned) {
98
15
    std::vector<V1IndexFile> files;
99
15
    if (file_info.index_info_size() == 0) {
100
15
        for (const TabletIndex* index : schema.inverted_and_ann_indexes()) {
101
15
            files.push_back({.index = index, .persisted_size = -1});
102
15
        }
103
12
        return files;
104
12
    }
105
5
    for (const auto& index_info : file_info.index_info()) {
106
5
        TabletIndexPB index_pb;
107
5
        index_pb.set_index_type(IndexType::INVERTED);
108
5
        index_pb.set_index_id(index_info.index_id());
109
5
        index_pb.set_index_suffix_name(index_info.index_suffix());
110
5
        owned->emplace_back().init_from_pb(index_pb);
111
5
        files.push_back({.index = &owned->back(),
112
5
                         .persisted_size = index_info.index_file_size() > 0
113
5
                                                   ? index_info.index_file_size()
114
5
                                                   : -1});
115
5
    }
116
3
    return files;
117
15
}
118
119
// Parses the index id and suffix out of `{segment}_{index_id}[@{suffix}].idx`, the name of a V1
120
// index file of the segment named `segment`.
121
std::optional<std::pair<int64_t, std::string>> parse_v1_index_file_name(std::string_view name,
122
11
                                                                        std::string_view segment) {
123
11
    constexpr std::string_view extension = InvertedIndexDescriptor::index_suffix;
124
11
    if (name.size() < segment.size() + extension.size() + 2 || !name.starts_with(segment) ||
125
11
        !name.ends_with(extension) || name[segment.size()] != '_') {
126
1
        return std::nullopt;
127
1
    }
128
10
    std::string_view rest = name.substr(segment.size() + 1);
129
10
    rest.remove_suffix(extension.size());
130
10
    uint64_t index_id = 0;
131
10
    const auto [digits_end, error] =
132
10
            std::from_chars(rest.data(), rest.data() + rest.size(), index_id);
133
10
    if (error != std::errc() || index_id > std::numeric_limits<int64_t>::max()) {
134
2
        return std::nullopt;
135
2
    }
136
8
    std::string_view tail = rest.substr(digits_end - rest.data());
137
8
    if (tail.empty()) {
138
3
        return std::make_pair(static_cast<int64_t>(index_id), std::string());
139
3
    }
140
5
    if (tail.front() != '@') {
141
0
        return std::nullopt;
142
0
    }
143
5
    return std::make_pair(static_cast<int64_t>(index_id), std::string(tail.substr(1)));
144
5
}
145
146
// A legacy local VARIANT rowset has no index file info, and its schema may not name every file
147
// that was written for an extracted path. Adds the V1 index files of the segment that exist in its
148
// directory and are not in `files` yet, in index id and suffix order.
149
Status add_v1_index_files_on_disk(const std::string& index_path_prefix,
150
                                  DirectoryFileNames* directory_files,
151
3
                                  std::deque<TabletIndex>* owned, std::vector<V1IndexFile>* files) {
152
3
    const std::filesystem::path prefix(index_path_prefix);
153
3
    const std::vector<std::string>* names = nullptr;
154
3
    RETURN_IF_ERROR(directory_files->list(prefix.parent_path().string(), &names));
155
3
    std::set<std::pair<int64_t, std::string>> listed;
156
3
    for (const V1IndexFile& file : *files) {
157
3
        listed.emplace(file.index->index_id(), file.index->get_index_suffix());
158
3
    }
159
    // The names are sorted, so the files of this segment are adjacent.
160
3
    const std::string segment_name = prefix.filename().string();
161
3
    const std::string name_prefix = segment_name + "_";
162
3
    std::set<std::pair<int64_t, std::string>> on_disk;
163
3
    for (auto it = std::lower_bound(names->begin(), names->end(), name_prefix);
164
14
         it != names->end() && it->starts_with(name_prefix); ++it) {
165
11
        if (auto parsed = parse_v1_index_file_name(*it, segment_name);
166
11
            parsed.has_value() && !listed.contains(*parsed)) {
167
5
            on_disk.insert(std::move(*parsed));
168
5
        }
169
11
    }
170
5
    for (const auto& [index_id, suffix] : on_disk) {
171
5
        TabletIndexPB index_pb;
172
5
        index_pb.set_index_type(IndexType::INVERTED);
173
5
        index_pb.set_index_id(index_id);
174
5
        index_pb.set_index_suffix_name(suffix);
175
5
        owned->emplace_back().init_from_pb(index_pb);
176
5
        files->push_back({.index = &owned->back(), .persisted_size = -1});
177
5
    }
178
3
    return Status::OK();
179
3
}
180
181
// Classifies every sub-file of a CLucene directory into `record` and adds their total length to
182
// `files_bytes`.
183
Status add_directory_files(const lucene::store::Directory& dir, IndexDiskUsageRecord* record,
184
33
                           int64_t* files_bytes) {
185
33
    try {
186
33
        std::vector<std::string> names;
187
33
        if (!dir.list(&names)) {
188
0
            return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
189
0
                    "failed to list inverted index sub-files");
190
0
        }
191
289
        for (const auto& name : names) {
192
289
            const int64_t length = dir.fileLength(name.c_str());
193
289
            classify_clucene_file(name, length, record);
194
289
            *files_bytes += length;
195
289
        }
196
33
    } catch (CLuceneError& e) {
197
0
        return Status::Error<ErrorCode::INVERTED_INDEX_CLUCENE_ERROR>(
198
0
                "failed to read inverted index sub-files: {}", e.what());
199
0
    }
200
33
    return Status::OK();
201
33
}
202
203
// Sums the position bytes of the dictionary entries whose postings live in the posting region.
204
// Inline postings stay in the dictionary region and are not counted.
205
Status sum_snii_position_bytes(const IndexFileReader& reader, uint64_t index_id,
206
                               std::string_view suffix, const IndexDiskUsageOptions& options,
207
3
                               int64_t* position_bytes) {
208
    // A full dictionary scan should not evict blocks that queries keep in the file cache.
209
3
    io::IOContext io_ctx = options.io_ctx != nullptr ? *options.io_ctx : io::IOContext {};
210
3
    io_ctx.is_disposable = true;
211
3
    io_ctx.is_inverted_index = true;
212
3
    auto logical = DORIS_TRY(reader.open_snii_logical_index(
213
3
            index_id, suffix, &io_ctx, snii::reader::LogicalIndexOpenMode::kCompaction));
214
3
    snii_doris::DorisSniiFileReader::ScopedIOContext io_context_scope(&io_ctx);
215
3
    std::vector<snii::format::DictEntry> entries;
216
7
    for (uint32_t block = 0; block < logical->n_dict_blocks(); ++block) {
217
5
        RETURN_IF_ERROR(check_cancelled(options));
218
4
        uint64_t frq_base = 0;
219
4
        uint64_t prx_base = 0;
220
4
        RETURN_IF_ERROR(logical->decode_dict_block(block, &entries, &frq_base, &prx_base));
221
2.41k
        for (const auto& entry : entries) {
222
2.41k
            if (entry.kind == snii::format::DictEntryKind::kPodRef) {
223
6
                *position_bytes += cast_set<int64_t>(entry.prx_len);
224
6
            }
225
2.41k
        }
226
4
    }
227
2
    return Status::OK();
228
3
}
229
230
65
int64_t merge_component(int64_t lhs, int64_t rhs) {
231
65
    return lhs < 0 || rhs < 0 ? -1 : lhs + rhs;
232
65
}
233
234
} // namespace
235
236
6
Status DirectoryFileNames::list(const std::string& dir, const std::vector<std::string>** names) {
237
6
    if (!_listed || _dir != dir) {
238
        // The file sizes are not read: unused rowsets delete their files from the tablet directory
239
        // while it is listed, and the stat of a vanished file would fail the listing. An entry whose
240
        // type cannot be read is gone and is skipped.
241
4
        std::error_code ec;
242
4
        std::filesystem::directory_iterator it(dir, ec);
243
4
        if (ec && ec != std::errc::no_such_file_or_directory) {
244
0
            return Status::IOError("failed to list {}: {}", dir, ec.message());
245
0
        }
246
4
        _names.clear();
247
24
        for (const std::filesystem::directory_iterator end; !ec && it != end; it.increment(ec)) {
248
20
            std::error_code type_ec;
249
20
            if (it->is_regular_file(type_ec)) {
250
17
                _names.push_back(it->path().filename().string());
251
17
            }
252
20
        }
253
4
        if (ec && ec != std::errc::no_such_file_or_directory) {
254
0
            return Status::IOError("failed to list {}: {}", dir, ec.message());
255
0
        }
256
4
        std::sort(_names.begin(), _names.end());
257
4
        _dir = dir;
258
4
        _listed = true;
259
4
        ++_listings;
260
4
    }
261
6
    *names = &_names;
262
6
    return Status::OK();
263
6
}
264
265
std::vector<IndexDiskUsageRow> aggregate_index_disk_usage(std::vector<IndexDiskUsageRow> rows,
266
6
                                                          IndexDiskUsageLevel level) {
267
6
    if (level == IndexDiskUsageLevel::kSegment) {
268
1
        return rows;
269
1
    }
270
5
    using Key = std::tuple<std::string, int64_t, std::string, int, int>;
271
5
    std::map<Key, size_t> positions;
272
5
    std::vector<IndexDiskUsageRow> merged;
273
21
    for (auto& row : rows) {
274
21
        row.segment_id = -1;
275
21
        if (level == IndexDiskUsageLevel::kTablet) {
276
17
            row.rowset_id.clear();
277
17
        }
278
21
        Key key {row.rowset_id, row.record.index_id, row.record.index_suffix,
279
21
                 static_cast<int>(row.format), static_cast<int>(row.record.structure)};
280
21
        auto [it, inserted] = positions.emplace(std::move(key), merged.size());
281
21
        if (inserted) {
282
8
            merged.push_back(std::move(row));
283
8
            continue;
284
8
        }
285
13
        IndexDiskUsageRow& target = merged[it->second];
286
13
        target.segment_count += row.segment_count;
287
13
        target.row_count += row.row_count;
288
13
        IndexDiskUsageRecord& dst = target.record;
289
13
        const IndexDiskUsageRecord& src = row.record;
290
13
        dst.total_bytes += src.total_bytes;
291
13
        dst.dict_bytes = merge_component(dst.dict_bytes, src.dict_bytes);
292
13
        dst.posting_bytes = merge_component(dst.posting_bytes, src.posting_bytes);
293
13
        dst.position_bytes = merge_component(dst.position_bytes, src.position_bytes);
294
13
        dst.stats_bytes = merge_component(dst.stats_bytes, src.stats_bytes);
295
13
        dst.other_bytes = merge_component(dst.other_bytes, src.other_bytes);
296
13
    }
297
5
    return merged;
298
6
}
299
300
304
void classify_clucene_file(std::string_view name, int64_t length, IndexDiskUsageRecord* record) {
301
304
    record->total_bytes += length;
302
304
    if (is_bkd_file(name)) {
303
15
        record->structure = IndexDiskUsageStructure::kBkd;
304
15
        return;
305
15
    }
306
289
    if (is_ann_file(name)) {
307
6
        record->structure = IndexDiskUsageStructure::kAnn;
308
6
        return;
309
6
    }
310
283
    const size_t dot = name.rfind('.');
311
283
    const std::string_view extension =
312
283
            dot == std::string_view::npos ? std::string_view() : name.substr(dot + 1);
313
283
    if (extension == "tis" || extension == "tii") {
314
56
        record->dict_bytes += length;
315
227
    } else if (extension == "frq") {
316
28
        record->posting_bytes += length;
317
199
    } else if (extension == "prx") {
318
27
        record->position_bytes += length;
319
172
    } else if (extension == "nrm") {
320
28
        record->stats_bytes += length;
321
144
    } else {
322
144
        record->other_bytes += length;
323
144
    }
324
283
}
325
326
IndexDiskUsageCollector::IndexDiskUsageCollector(io::FileSystemSPtr fs,
327
                                                 std::string index_path_prefix,
328
                                                 TabletSchemaSPtr schema,
329
                                                 InvertedIndexStorageFormatPB format,
330
                                                 int64_t tablet_id,
331
                                                 InvertedIndexFileInfo index_file_info)
332
45
        : _fs(std::move(fs)),
333
45
          _index_path_prefix(std::move(index_path_prefix)),
334
45
          _schema(std::move(schema)),
335
45
          _format(format),
336
45
          _tablet_id(tablet_id),
337
45
          _index_file_info(std::move(index_file_info)) {}
338
339
Status IndexDiskUsageCollector::collect(const IndexDiskUsageOptions& options,
340
45
                                        std::vector<IndexDiskUsageRecord>* out) {
341
45
    DORIS_CHECK(out != nullptr);
342
    // A rowset returns no file system when its tablet or storage resource cannot be resolved.
343
45
    if (_fs == nullptr) {
344
3
        return Status::Error<ErrorCode::INIT_FAILED>("no file system for inverted index files {}",
345
3
                                                     _index_path_prefix);
346
3
    }
347
42
    switch (_format) {
348
15
    case InvertedIndexStorageFormatPB::V1:
349
15
        return _collect_v1(options, out);
350
12
    case InvertedIndexStorageFormatPB::V2:
351
15
    case InvertedIndexStorageFormatPB::V3:
352
15
        return _collect_compound(options, out);
353
12
    case InvertedIndexStorageFormatPB::SNII:
354
12
        return _collect_snii(options, out);
355
0
    default:
356
0
        return Status::NotSupported("index disk usage does not support inverted index format {}",
357
0
                                    InvertedIndexStorageFormatPB_Name(_format));
358
42
    }
359
42
}
360
361
Status IndexDiskUsageCollector::_collect_v1(const IndexDiskUsageOptions& options,
362
15
                                            std::vector<IndexDiskUsageRecord>* out) {
363
15
    IndexFileReader reader(_fs, _index_path_prefix, _format, _index_file_info, _tablet_id);
364
15
    RETURN_IF_ERROR(reader.init(config::inverted_index_read_buffer_size, options.io_ctx));
365
15
    std::deque<TabletIndex> file_indexes;
366
15
    std::vector<V1IndexFile> v1_files =
367
15
            list_v1_index_files(*_schema, _index_file_info, &file_indexes);
368
15
    if (_index_file_info.index_info_size() == 0 && _schema->num_variant_columns() > 0 &&
369
15
        _fs->type() == io::FileSystemType::LOCAL) {
370
3
        DirectoryFileNames own_listing;
371
3
        RETURN_IF_ERROR(add_v1_index_files_on_disk(
372
3
                _index_path_prefix,
373
3
                options.directory_files != nullptr ? options.directory_files : &own_listing,
374
3
                &file_indexes, &v1_files));
375
3
    }
376
25
    for (const V1IndexFile& file : v1_files) {
377
25
        const TabletIndex& index = *file.index;
378
25
        if (!is_wanted(options, index.index_id())) {
379
0
            continue;
380
0
        }
381
25
        RETURN_IF_ERROR(check_cancelled(options));
382
        // The rowset meta records only files that were written, so only a file listed from the
383
        // schema may be absent.
384
24
        const bool recorded = file.persisted_size > 0;
385
24
        int64_t file_size = file.persisted_size;
386
24
        if (!recorded) {
387
19
            const std::string path = InvertedIndexDescriptor::get_index_file_path_v1(
388
19
                    _index_path_prefix, index.index_id(), index.get_index_suffix());
389
19
            const Status size_status = _fs->file_size(path, &file_size);
390
19
            if (is_absent_index_file(size_status)) {
391
1
                continue;
392
1
            }
393
18
            RETURN_IF_ERROR(size_status);
394
18
        }
395
23
        auto directory = reader.open(&index, options.io_ctx);
396
23
        if (!directory.has_value()) {
397
1
            if (!recorded && is_absent_index_file(directory.error())) {
398
0
                continue;
399
0
            }
400
1
            return directory.error();
401
1
        }
402
403
22
        IndexDiskUsageRecord record;
404
22
        record.index_id = index.index_id();
405
22
        record.index_suffix = index.get_index_suffix();
406
22
        int64_t files_bytes = 0;
407
22
        RETURN_IF_ERROR(add_directory_files(*directory.value(), &record, &files_bytes));
408
        // Each V1 index owns its file, so its compound header is part of the index.
409
22
        record.other_bytes += file_size - files_bytes;
410
22
        record.total_bytes += file_size - files_bytes;
411
22
        out->push_back(std::move(record));
412
22
    }
413
13
    return Status::OK();
414
15
}
415
416
Status IndexDiskUsageCollector::_collect_compound(const IndexDiskUsageOptions& options,
417
15
                                                  std::vector<IndexDiskUsageRecord>* out) {
418
15
    IndexFileReader reader(_fs, _index_path_prefix, _format, _index_file_info, _tablet_id);
419
15
    if (const Status st = reader.init(config::inverted_index_read_buffer_size, options.io_ctx);
420
15
        !st.ok()) {
421
5
        return is_skipped_container(st, _index_file_info) ? Status::OK() : st;
422
5
    }
423
10
    auto directories = DORIS_TRY(reader.get_all_directories());
424
425
    // Every index counts toward the attributed bytes, even the filtered ones, so the container
426
    // record only holds the shared header.
427
10
    int64_t attributed_bytes = 0;
428
11
    for (const auto& [key, directory] : directories) {
429
11
        IndexDiskUsageRecord record;
430
11
        record.index_id = key.first;
431
11
        record.index_suffix = key.second;
432
11
        RETURN_IF_ERROR(add_directory_files(*directory, &record, &attributed_bytes));
433
11
        if (is_wanted(options, record.index_id)) {
434
10
            out->push_back(std::move(record));
435
10
        }
436
11
    }
437
10
    if (options.index_ids.empty()) {
438
9
        IndexDiskUsageRecord container;
439
9
        container.structure = IndexDiskUsageStructure::kContainer;
440
9
        container.total_bytes = reader.get_inverted_file_size() - attributed_bytes;
441
9
        container.other_bytes = container.total_bytes;
442
9
        out->push_back(std::move(container));
443
9
    }
444
10
    return Status::OK();
445
10
}
446
447
Status IndexDiskUsageCollector::_collect_snii(const IndexDiskUsageOptions& options,
448
12
                                              std::vector<IndexDiskUsageRecord>* out) {
449
12
    IndexFileReader reader(_fs, _index_path_prefix, _format, _index_file_info, _tablet_id);
450
12
    if (const Status st = reader.init(config::inverted_index_read_buffer_size, options.io_ctx);
451
12
        !st.ok()) {
452
3
        return is_skipped_container(st, _index_file_info) ? Status::OK() : st;
453
3
    }
454
9
    const auto entries = DORIS_TRY(reader.snii_logical_indexes());
455
456
9
    int64_t attributed_bytes = 0;
457
9
    for (const auto& entry : entries) {
458
9
        const auto index_id = cast_set<int64_t>(entry.index_id);
459
        // The container record is only reported without a filter, so filtered-out indexes
460
        // need no metadata read.
461
9
        if (!is_wanted(options, index_id)) {
462
0
            continue;
463
0
        }
464
9
        RETURN_IF_ERROR(check_cancelled(options));
465
8
        IndexDiskUsageRecord record;
466
8
        record.index_id = index_id;
467
8
        record.index_suffix = entry.index_suffix;
468
8
        if (entry.kind == snii::format::LogicalIndexKind::kInverted) {
469
7
            snii::format::CoreMetadata core;
470
7
            RETURN_IF_ERROR(reader.snii_core_metadata(entry.index_id, entry.index_suffix, &core,
471
7
                                                      options.io_ctx));
472
7
            const auto& refs = core.section_refs;
473
7
            record.structure = IndexDiskUsageStructure::kTerm;
474
7
            record.dict_bytes =
475
7
                    cast_set<int64_t>(refs.dict_region.length + entry.sampled_term_index.length +
476
7
                                      entry.dict_block_directory.length + refs.bsbf.length);
477
7
            record.posting_bytes = cast_set<int64_t>(refs.posting_region.length);
478
7
            record.stats_bytes = cast_set<int64_t>(entry.core_metadata.length + refs.norms.length);
479
7
            record.other_bytes = cast_set<int64_t>(refs.null_bitmap.length);
480
7
            if (!snii::format::has_positions(core.index_config)) {
481
1
                record.position_bytes = 0;
482
6
            } else if (options.position_detail) {
483
3
                int64_t position_bytes = 0;
484
3
                RETURN_IF_ERROR(sum_snii_position_bytes(reader, entry.index_id, entry.index_suffix,
485
3
                                                        options, &position_bytes));
486
2
                record.position_bytes = position_bytes;
487
2
                record.posting_bytes -= position_bytes;
488
3
            } else {
489
3
                record.position_bytes = -1;
490
3
            }
491
6
            record.total_bytes = record.dict_bytes + record.posting_bytes +
492
6
                                 std::max<int64_t>(record.position_bytes, 0) + record.stats_bytes +
493
6
                                 record.other_bytes;
494
6
        } else {
495
1
            record.structure = entry.kind == snii::format::LogicalIndexKind::kBkd
496
1
                                       ? IndexDiskUsageStructure::kBkd
497
1
                                       : IndexDiskUsageStructure::kAnn;
498
3
            for (const auto& file : entry.files) {
499
3
                record.total_bytes += cast_set<int64_t>(file.length);
500
3
            }
501
1
        }
502
7
        attributed_bytes += record.total_bytes;
503
7
        out->push_back(std::move(record));
504
7
    }
505
7
    if (options.index_ids.empty()) {
506
7
        IndexDiskUsageRecord container;
507
7
        container.structure = IndexDiskUsageStructure::kContainer;
508
7
        container.total_bytes = reader.get_inverted_file_size() - attributed_bytes;
509
7
        container.other_bytes = container.total_bytes;
510
7
        out->push_back(std::move(container));
511
7
    }
512
7
    return Status::OK();
513
9
}
514
515
const TabletIndex* resolve_disk_usage_index(const TabletSchema& schema, int64_t index_id,
516
5
                                            std::string_view suffix) {
517
    // An extracted VARIANT path keeps its parent index id under a path suffix that the current
518
    // schema may not list, so an unmatched suffix resolves to the index of the same id.
519
5
    const TabletIndex* same_id = nullptr;
520
10
    for (const TabletIndex* index : schema.inverted_and_ann_indexes()) {
521
10
        if (index->index_id() != index_id) {
522
5
            continue;
523
5
        }
524
5
        if (index->get_index_suffix() == suffix) {
525
3
            return index;
526
3
        }
527
2
        if (same_id == nullptr || index->get_index_suffix().empty()) {
528
2
            same_id = index;
529
2
        }
530
2
    }
531
2
    return same_id;
532
5
}
533
534
Status collect_rowset_index_disk_usage(const RowsetSharedPtr& rowset,
535
                                       const IndexDiskUsageOptions& options, int64_t tablet_id,
536
2
                                       std::vector<IndexDiskUsageRow>* rows) {
537
2
    const TabletSchemaSPtr schema = rowset->tablet_schema();
538
2
    if (!schema->has_inverted_or_ann_index()) {
539
0
        return Status::OK();
540
0
    }
541
2
    const InvertedIndexStorageFormatPB format = schema->get_inverted_index_storage_format();
542
2
    const std::string rowset_id = rowset->rowset_id().to_string();
543
2
    for (auto segment : rowset->segments()) {
544
2
        RETURN_IF_ERROR(check_cancelled(options));
545
2
        const std::string segment_path = DORIS_TRY(segment.path());
546
2
        IndexDiskUsageCollector collector(
547
2
                rowset->rowset_meta()->fs(),
548
2
                std::string(InvertedIndexDescriptor::get_index_file_path_prefix(segment_path)),
549
2
                schema, format, tablet_id, segment.inverted_index_file_info());
550
2
        std::vector<IndexDiskUsageRecord> records;
551
2
        RETURN_IF_ERROR(collector.collect(options, &records));
552
2
        if (records.empty()) {
553
0
            continue;
554
0
        }
555
2
        int64_t row_count = 0;
556
2
        if (segment.has_num_rows()) {
557
0
            row_count = segment.num_rows();
558
2
        } else {
559
            // Rowsets without persisted row counts read them from the segment footer. Loading the
560
            // segment directly keeps a transient failure out of the rowset's run-once row cache.
561
2
            SegmentSharedPtr loaded;
562
2
            OlapReaderStatistics stats;
563
2
            RETURN_IF_ERROR(std::static_pointer_cast<BetaRowset>(rowset)->load_segment(
564
2
                    segment.ref(), &stats, &loaded, options.io_ctx));
565
1
            row_count = loaded->num_rows();
566
1
        }
567
2
        for (auto& record : records) {
568
2
            IndexDiskUsageRow row;
569
2
            row.rowset_id = rowset_id;
570
2
            row.segment_id = cast_set<int32_t>(segment.id());
571
2
            row.segment_count = 1;
572
2
            row.row_count = row_count;
573
2
            row.format = format;
574
2
            row.record = std::move(record);
575
2
            rows->push_back(std::move(row));
576
2
        }
577
1
    }
578
1
    return Status::OK();
579
2
}
580
581
} // namespace doris::segment_v2