cobalt/metrics: Add browser tests for granular memory tracking#10020
cobalt/metrics: Add browser tests for granular memory tracking#10020Awallky wants to merge 8 commits intoyoutube:mainfrom
Conversation
# Conflicts: # services/resource_coordinator/public/cpp/memory_instrumentation/os_metrics_linux.cc
🤖 Gemini Suggested Commit Message💡 Pro Tips for a Better Commit Message:
|
There was a problem hiding this comment.
Code Review
This pull request implements granular memory reporting for Cobalt by introducing the DetailedMetricsDelegate interface and refactoring Linux memory parsing to use more efficient string utilities. Key changes include replacing sscanf with base::SplitStringPiece and updating OSMetrics to support project-specific memory categorization. Review feedback highlights opportunities to improve parsing robustness, such as using base::ReadFileToString to handle long lines in /proc/PID/smaps and employing pointer arithmetic for token extraction. Additionally, a suggestion was made to ensure that PSS validation failures do not prevent the reporting of basic memory metrics.
| FILE* f = nullptr; | ||
| bool should_close = false; | ||
| { | ||
| base::AutoLock lock(GetTestingGlobalsLock()); | ||
| if (g_proc_smaps_for_testing) { | ||
| f = g_proc_smaps_for_testing; | ||
| fseek(f, 0, SEEK_SET); | ||
| } | ||
| } | ||
|
|
||
| if (!f) { | ||
| std::string file_name = | ||
| "/proc/" + | ||
| (handle == base::kNullProcessHandle ? "self" | ||
| : base::NumberToString(handle)) + | ||
| "/smaps"; | ||
| base::ScopedFILE smaps_file(fopen(file_name.c_str(), "r")); | ||
| res = ReadLinuxProcSmapsFile(smaps_file.get(), &maps); | ||
| f = fopen(file_name.c_str(), "r"); | ||
| if (!f) { | ||
| DPLOG(ERROR) << "Couldn't open " << file_name; | ||
| return false; | ||
| } | ||
| should_close = true; | ||
| } | ||
|
|
||
| if (!res) | ||
| return std::vector<VmRegionPtr>(); | ||
| char line[2048]; | ||
| while (fgets(line, sizeof(line), f)) { | ||
| if (!delegate->OnSmapsBuffer(line)) { | ||
| if (should_close) { | ||
| fclose(f); | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return maps; | ||
| if (should_close) { | ||
| fclose(f); | ||
| } else { | ||
| fseek(f, 0, SEEK_SET); | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
The current implementation uses fgets with a 2048-byte buffer. If a line in /proc/PID/smaps (e.g., a very long path) exceeds this size, it will be split into multiple calls to OnSmapsBuffer, which will likely break the delegate's parsing logic.
Additionally, the previous implementation of ReadLinuxProcSmapsFile used a 4096-byte buffer. It is recommended to use base::ReadFileToString and base::SplitStringPiece to handle lines of any length correctly and to be consistent with GetProcessMemoryMaps in this same file.
std::string smaps_content;
{
base::AutoLock lock(GetTestingGlobalsLock());
if (g_proc_smaps_for_testing) {
fseek(g_proc_smaps_for_testing, 0, SEEK_SET);
if (!base::ReadStreamToString(g_proc_smaps_for_testing, &smaps_content)) {
return false;
}
}
}
if (smaps_content.empty()) {
std::string file_name =
"/proc/" +
(handle == base::kNullProcessHandle ? "self"
: base::NumberToString(handle)) +
"/smaps";
if (!base::ReadFileToString(base::FilePath(file_name), &smaps_content)) {
DPLOG(ERROR) << "Couldn't read " << file_name;
return false;
}
}
for (std::string_view line : base::SplitStringPiece(
smaps_content, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL)) {
if (line.empty()) continue;
if (!delegate->OnSmapsBuffer(line)) return false;
}
return true;| // We find the inode token in the original line, but we must search | ||
| // after the device token to avoid matching the same string in earlier | ||
| // fields (e.g. if the offset is the same as the inode). | ||
| size_t dev_pos = lines[i].find(tokens[3]); | ||
| size_t inode_pos = lines[i].find(tokens[4], dev_pos + tokens[3].size()); | ||
| if (inode_pos != std::string_view::npos) { | ||
| size_t path_pos = | ||
| lines[i].find_first_not_of(' ', inode_pos + tokens[4].size()); | ||
| if (path_pos != std::string_view::npos) { | ||
| region.path.assign(lines[i].substr(path_pos)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Instead of using find() to locate the inode token in the line (which requires searching after the device token to avoid false positives), you can use pointer arithmetic since tokens[4] is a std::string_view pointing into lines[i]. This is more robust and efficient.
size_t inode_pos = tokens[4].data() - lines[i].data();
size_t path_pos =
lines[i].find_first_not_of(' ', inode_pos + tokens[4].size());
if (path_pos != std::string_view::npos) {
region.path.assign(lines[i].substr(path_pos));
}| if (tokens.size() >= 6) { | ||
| size_t dev_pos = header_line.find(tokens[3]); | ||
| if (dev_pos != std::string_view::npos) { | ||
| size_t inode_pos = | ||
| header_line.find(tokens[4], dev_pos + tokens[3].size()); | ||
| if (inode_pos != std::string_view::npos) { | ||
| size_t filename_pos = | ||
| header_line.find_first_not_of(' ', inode_pos + tokens[4].size()); | ||
| if (filename_pos != std::string_view::npos) { | ||
| region->mapped_file = std::string(header_line.substr(filename_pos)); | ||
| base::TrimWhitespaceASCII(region->mapped_file, base::TRIM_ALL, | ||
| ®ion->mapped_file); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Similar to the change in proc_maps_linux.cc, you can use pointer arithmetic to find the exact position of the inode token in header_line. This avoids the need for find() and the logic to search after dev_pos.
if (tokens.size() >= 6) {
size_t inode_pos = tokens[4].data() - header_line.data();
size_t filename_pos =
header_line.find_first_not_of(' ', inode_pos + tokens[4].size());
if (filename_pos != std::string_view::npos) {
region->mapped_file = std::string(header_line.substr(filename_pos));
base::TrimWhitespaceASCII(region->mapped_file, base::TRIM_ALL,
®ion->mapped_file);
}
}| if (metrics.total_pss_kb > 0 && abs_diff > kPssValidationThresholdKb) { | ||
| LOG(WARNING) << "Detailed metrics PSS validation failed. " | ||
| << "Detailed: " << metrics.total_pss_kb | ||
| << " KiB, Rollup: " << rollup_pss / 1024 << " KiB"; | ||
| return false; | ||
| } | ||
| dump->detailed_stats_kb = std::move(metrics.categories_kb); | ||
| dump->last_detailed_dump_time = base::TimeTicks::Now(); |
There was a problem hiding this comment.
If the PSS validation against smaps_rollup fails, returning false here causes the entire memory dump for this PID to be discarded in client_process_impl.cc. It would be more robust to log the warning but still return true, simply skipping the population of detailed_stats_kb so that basic metrics are still reported.
| if (metrics.total_pss_kb > 0 && abs_diff > kPssValidationThresholdKb) { | |
| LOG(WARNING) << "Detailed metrics PSS validation failed. " | |
| << "Detailed: " << metrics.total_pss_kb | |
| << " KiB, Rollup: " << rollup_pss / 1024 << " KiB"; | |
| return false; | |
| } | |
| dump->detailed_stats_kb = std::move(metrics.categories_kb); | |
| dump->last_detailed_dump_time = base::TimeTicks::Now(); | |
| if (metrics.total_pss_kb > 0 && abs_diff > kPssValidationThresholdKb) { | |
| LOG(WARNING) << "Detailed metrics PSS validation failed. " | |
| << "Detailed: " << metrics.total_pss_kb | |
| << " KiB, Rollup: " << rollup_pss / 1024 << " KiB"; | |
| } else { | |
| dump->detailed_stats_kb = std::move(metrics.categories_kb); | |
| dump->last_detailed_dump_time = base::TimeTicks::Now(); | |
| } | |
| return true; |
This commit adds integration and browser tests to verify that the granular
memory tracking system works as expected in a browser environment.
GranularMemoryBrowsertestto verify dump triggering and propagation.Bug: 494004530