blob: f0cce6a2848b9ee055a93acb2118f8f9b0fa08a7 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "perfetto/ext/profiling/smaps.h"
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <string>
#include <string_view>
#include <vector>
#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/protozero/packed_repeated_fields.h"
#include "protos/perfetto/trace/profiling/smaps.pbzero.h"
#include "protos/perfetto/trace/trace_packet.pbzero.h"
namespace perfetto {
namespace profiling {
namespace {
class StringInterner {
public:
using StringId = size_t;
StringInterner() {
// index zero is always the empty string
Intern(std::string_view{});
}
StringId Intern(std::string_view s) {
if (auto* p = map_.Find(s); p) {
return *p;
}
size_t index = storage_.size();
storage_.emplace_back(s);
std::string_view stable_sv(storage_.back());
map_.Insert(stable_sv, index);
return index;
}
const std::deque<std::string>& OrderedStrings() const { return storage_; }
private:
base::FlatHashMap<std::string_view, StringId> map_;
std::deque<std::string> storage_;
};
struct Vma {
StringInterner::StringId name_id = 0;
uint32_t aggregate_count = 1;
uint64_t size_kb = 0;
uint64_t rss_kb = 0;
uint64_t anonymous_kb = 0;
uint64_t swap_kb = 0;
uint64_t shared_clean_kb = 0;
uint64_t shared_dirty_kb = 0;
uint64_t private_clean_kb = 0;
uint64_t private_dirty_kb = 0;
uint64_t locked_kb = 0;
uint64_t pss_kb = 0;
uint64_t pss_dirty_kb = 0;
uint64_t swap_pss_kb = 0;
};
// clang-format off
enum SmapsField : uint32_t {
kSize = 1 << 0,
kRss = 1 << 1,
kAnonymous = 1 << 2,
kSwap = 1 << 3,
kSharedClean = 1 << 4,
kSharedDirty = 1 << 5,
kPrivateClean = 1 << 6,
kPrivateDirty = 1 << 7,
kLocked = 1 << 8,
kPss = 1 << 9,
kPssDirty = 1 << 10,
kSwapPss = 1 << 11,
};
// clang-format on
// Convenience mapping between config proto enums, implementation bitflags,
// field offsets, and trace proto field ids.
struct SmapsFieldDef {
SmapsField flag;
uint64_t Vma::* member_ptr;
int32_t config_pb_enum;
uint32_t trace_field_id;
};
using SC = protos::gen::SmapsConfig;
using SP = protos::pbzero::PackedSmaps;
// clang-format off
constexpr SmapsFieldDef kSmapsFieldDefs[] = {
{kSize, &Vma::size_kb, SC::VMA_FIELD_SIZE, SP::kSizeKbFieldNumber},
{kRss, &Vma::rss_kb, SC::VMA_FIELD_RSS, SP::kRssKbFieldNumber},
{kAnonymous, &Vma::anonymous_kb, SC::VMA_FIELD_ANONYMOUS, SP::kAnonymousKbFieldNumber},
{kSwap, &Vma::swap_kb, SC::VMA_FIELD_SWAP, SP::kSwapKbFieldNumber},
{kSharedClean, &Vma::shared_clean_kb, SC::VMA_FIELD_SHARED_CLEAN, SP::kSharedCleanKbFieldNumber},
{kSharedDirty, &Vma::shared_dirty_kb, SC::VMA_FIELD_SHARED_DIRTY, SP::kSharedDirtyKbFieldNumber},
{kPrivateClean, &Vma::private_clean_kb, SC::VMA_FIELD_PRIVATE_CLEAN, SP::kPrivateCleanKbFieldNumber},
{kPrivateDirty, &Vma::private_dirty_kb, SC::VMA_FIELD_PRIVATE_DIRTY, SP::kPrivateDirtyKbFieldNumber},
{kLocked, &Vma::locked_kb, SC::VMA_FIELD_LOCKED, SP::kLockedKbFieldNumber},
{kPss, &Vma::pss_kb, SC::VMA_FIELD_PSS, SP::kPssKbFieldNumber},
{kPssDirty, &Vma::pss_dirty_kb, SC::VMA_FIELD_PSS_DIRTY, SP::kPssDirtyKbFieldNumber},
{kSwapPss, &Vma::swap_pss_kb, SC::VMA_FIELD_SWAP_PSS, SP::kSwapPssKbFieldNumber},
};
// clang-format on
void AggregateVma(Vma& dest, const Vma& src) {
dest.aggregate_count += src.aggregate_count;
for (const auto& field : kSmapsFieldDefs) {
dest.*(field.member_ptr) += src.*(field.member_ptr);
}
}
std::string_view ExtractMappingName(std::string_view line) {
// Skip until the last space-delimited column, which can itself contain spaces
// so we can't tokenise from the end.
size_t pos = 0;
for (int i = 0; i < 5; ++i) {
if (pos = line.find(' ', pos); pos == std::string_view::npos)
return {};
if (pos = line.find_first_not_of(' ', pos); pos == std::string_view::npos)
return {};
}
size_t end = line.size() - 1; // loop above guarantees size > 0
if (pos >= end)
return {};
return line.substr(pos, end - pos);
}
void ParseSmapsLine(const char* line, Vma& vma, uint32_t& fields) {
if (!line)
return;
// Note: strtoull skips leading spaces and the "kB" suffix. This is not
// interchangeable with base::CStringToUInt64.
switch (line[0]) {
case 'S': {
if ((fields & kSize) && strncmp(line, "Size:", 5) == 0) {
vma.size_kb = std::strtoull(line + 5, nullptr, 10);
fields &= ~kSize;
} else if ((fields & kSwap) && strncmp(line, "Swap:", 5) == 0) {
vma.swap_kb = std::strtoull(line + 5, nullptr, 10);
fields &= ~kSwap;
} else if ((fields & kSwapPss) && strncmp(line, "SwapPss:", 8) == 0) {
vma.swap_pss_kb = std::strtoull(line + 8, nullptr, 10);
fields &= ~kSwapPss;
} else if ((fields & kSharedClean) &&
strncmp(line, "Shared_Clean:", 13) == 0) {
vma.shared_clean_kb = std::strtoull(line + 13, nullptr, 10);
fields &= ~kSharedClean;
} else if ((fields & kSharedDirty) &&
strncmp(line, "Shared_Dirty:", 13) == 0) {
vma.shared_dirty_kb = std::strtoull(line + 13, nullptr, 10);
fields &= ~kSharedDirty;
}
break;
}
case 'R': {
if ((fields & kRss) && strncmp(line, "Rss:", 4) == 0) {
vma.rss_kb = std::strtoull(line + 4, nullptr, 10);
fields &= ~kRss;
}
break;
}
case 'A': {
if ((fields & kAnonymous) && strncmp(line, "Anonymous:", 10) == 0) {
vma.anonymous_kb = std::strtoull(line + 10, nullptr, 10);
fields &= ~kAnonymous;
}
break;
}
case 'P': {
if ((fields & kPss) && strncmp(line, "Pss:", 4) == 0) {
vma.pss_kb = std::strtoull(line + 4, nullptr, 10);
fields &= ~kPss;
} else if ((fields & kPssDirty) && strncmp(line, "Pss_Dirty:", 10) == 0) {
vma.pss_dirty_kb = std::strtoull(line + 10, nullptr, 10);
fields &= ~kPssDirty;
} else if ((fields & kPrivateClean) &&
strncmp(line, "Private_Clean:", 14) == 0) {
vma.private_clean_kb = std::strtoull(line + 14, nullptr, 10);
fields &= ~kPrivateClean;
} else if ((fields & kPrivateDirty) &&
strncmp(line, "Private_Dirty:", 14) == 0) {
vma.private_dirty_kb = std::strtoull(line + 14, nullptr, 10);
fields &= ~kPrivateDirty;
}
break;
}
case 'L': {
if ((fields & kLocked) && strncmp(line, "Locked:", 7) == 0) {
vma.locked_kb = std::strtoull(line + 7, nullptr, 10);
fields &= ~kLocked;
}
break;
}
default:
break;
}
}
template <typename FN>
void Parse(FILE* file,
StringInterner& interner,
uint32_t requested_fields,
FN callback) {
Vma vma = Vma{};
bool in_vma = false;
// bitmask of the fields that still need to be parsed for the current vma
uint32_t fields_to_parse = 0;
// getline (re)allocates the buffer, so free it when done
char* buf = nullptr;
auto getline_cleanup = base::OnScopeExit([&] { free(buf); });
size_t buf_len = 0;
ssize_t read_len = 0;
while ((read_len = getline(&buf, &buf_len, file)) != -1) {
std::string_view line(buf, static_cast<size_t>(read_len));
if (line.empty())
continue;
// Test if we're at a new vma boundary by checking that this isn't a
// colon-delimited line. Example of the two types of line:
// 7f13720e6000-7f13720e8000 r-xp 00000000 00:00 0 [vdso]
// Size: 8 kB
// Rss: 8 kB
size_t space_pos = line.find(' ');
size_t colon_pos = line.find(':');
if (colon_pos == std::string_view::npos || space_pos < colon_pos) {
if (in_vma) {
callback(vma);
}
vma = Vma{};
vma.name_id = interner.Intern(ExtractMappingName(line));
in_vma = true;
fields_to_parse = requested_fields;
} else if (in_vma) {
if (!fields_to_parse) {
continue; // done, skip until the next vma
}
ParseSmapsLine(buf, vma, fields_to_parse);
}
}
if (in_vma) {
callback(vma);
}
}
} // namespace
void ParseAndSerializeSmaps(FILE* file,
const protos::gen::SmapsConfig& config,
protos::pbzero::SmapsPacket* packet) {
if (!file || !packet)
return;
// Config -> bitmask of fields to collect.
uint32_t parser_mask = kSize | kRss | kAnonymous | kSwap;
if (config.vma_fields_size()) {
parser_mask = 0;
for (int32_t pb_enum : config.vma_fields()) {
for (const auto& def : kSmapsFieldDefs) {
if (def.config_pb_enum == pb_enum) {
parser_mask |= def.flag;
}
}
}
}
bool aggregated = !config.unaggregated();
// Parse the file:
StringInterner interner;
std::vector<Vma> vmas;
// If we're aggregating by name, use the vector as a map with the interned
// name as the index (since the StringInterner assigns ids in a sequential
// order).
// So since the interner always assigns the empty string the id 0,
// pre-create that vector entry.
if (aggregated) {
vmas.push_back(Vma{});
vmas[0].aggregate_count = 0;
}
Parse(file, interner, parser_mask, [&vmas, aggregated](Vma vma) {
if (!aggregated) {
vmas.push_back(vma);
return;
}
// aggregated: index into vector with interned id.
size_t name_id = vma.name_id;
if (name_id < vmas.size()) {
AggregateVma(vmas[name_id], vma);
} else {
vmas.resize(name_id + 1);
vmas[name_id] = vma;
}
});
// Serialise the proto:
auto packed_smaps = packet->set_packed_entries();
// string_table
for (const auto& v : interner.OrderedStrings()) {
packed_smaps->add_string_table(v);
}
protozero::PackedVarInt packed;
// If aggregating: write aggregate_count, but skip name_id as a size
// optimisation. We write the vmas exactly in string_table order, so the
// serialised name_id would be 0, 1, 2, 3, ...
if (aggregated) {
packed.Reset();
for (const auto& vma : vmas) {
packed.Append(vma.aggregate_count);
}
packed_smaps->set_aggregate_count(packed);
} else {
// Unaggregated: write name_id.
for (const auto& vma : vmas) {
packed.Append(static_cast<uint32_t>(vma.name_id));
}
packed_smaps->set_name_id(packed);
}
// write value fields
for (const auto& field : kSmapsFieldDefs) {
if (parser_mask & field.flag) {
packed.Reset();
for (const auto& vma : vmas) {
packed.Append(vma.*(field.member_ptr));
}
packed_smaps->AppendBytes(field.trace_field_id, packed.data(),
packed.size());
}
}
}
} // namespace profiling
} // namespace perfetto