summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYabin Cui <yabinc@google.com>2023-09-02 00:50:58 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-09-02 00:50:58 +0000
commit0fb55994afa466dc06eade79276cd0f77c26745b (patch)
tree4273bbf05b08a285e8738091b8897d1a638cdb84
parentbe12e2b4fe52db674bc27f307ff3b0703a642746 (diff)
parent0f0aecc178b6f01223d14b06206a766b56aa55e1 (diff)
downloadextras-0fb55994afa466dc06eade79276cd0f77c26745b.tar.gz
Merge changes Ie7cf3774,Ia3cbaf04,Ide7496a8 into main am: 0f0aecc178
Original change: https://android-review.googlesource.com/c/platform/system/extras/+/2736161 Change-Id: If60a902e542603b280c2d1dcb67634ebb1e3128d Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--simpleperf/RecordReadThread.cpp8
-rw-r--r--simpleperf/RecordReadThread.h6
-rw-r--r--simpleperf/RecordReadThread_test.cpp6
-rw-r--r--simpleperf/cmd_monitor.cpp2
-rw-r--r--simpleperf/cmd_record.cpp61
-rw-r--r--simpleperf/cmd_stat.cpp12
-rw-r--r--simpleperf/cmd_stat_impl.h23
-rw-r--r--simpleperf/doc/README.md46
-rw-r--r--simpleperf/event_selection_set.cpp4
-rw-r--r--simpleperf/event_selection_set.h2
-rw-r--r--simpleperf/utils.cpp13
-rw-r--r--simpleperf/utils.h2
-rw-r--r--simpleperf/utils_test.cpp7
13 files changed, 123 insertions, 69 deletions
diff --git a/simpleperf/RecordReadThread.cpp b/simpleperf/RecordReadThread.cpp
index 3e492ce2..2ab61278 100644
--- a/simpleperf/RecordReadThread.cpp
+++ b/simpleperf/RecordReadThread.cpp
@@ -223,7 +223,7 @@ bool KernelRecordReader::MoveToNextRecord(const RecordParser& parser) {
RecordReadThread::RecordReadThread(size_t record_buffer_size, const perf_event_attr& attr,
size_t min_mmap_pages, size_t max_mmap_pages,
- size_t aux_buffer_size, bool allow_cutting_samples,
+ size_t aux_buffer_size, bool allow_truncating_samples,
bool exclude_perf)
: record_buffer_(record_buffer_size),
record_parser_(attr),
@@ -239,7 +239,7 @@ RecordReadThread::RecordReadThread(size_t record_buffer_size, const perf_event_a
LOG(VERBOSE) << "user buffer size = " << record_buffer_size
<< ", low_level size = " << record_buffer_low_level_
<< ", critical_level size = " << record_buffer_critical_level_;
- if (!allow_cutting_samples) {
+ if (!allow_truncating_samples) {
record_buffer_low_level_ = record_buffer_critical_level_;
}
if (exclude_perf) {
@@ -538,7 +538,7 @@ void RecordReadThread::PushRecordToRecordBuffer(KernelRecordReader* kernel_recor
}
size_t stack_size_limit = stack_size_in_sample_record_;
if (free_size < record_buffer_low_level_) {
- // When the free size in record buffer is below low level, cut the stack data in sample
+ // When the free size in record buffer is below low level, truncate the stack data in sample
// records to 1K. This makes the unwinder unwind only part of the callchains, but hopefully
// the call chain joiner can complete the callchains.
stack_size_limit = 1024;
@@ -580,7 +580,7 @@ void RecordReadThread::PushRecordToRecordBuffer(KernelRecordReader* kernel_recor
memcpy(p + pos + new_stack_size, &new_stack_size, sizeof(uint64_t));
record_buffer_.FinishWrite();
if (new_stack_size < dyn_stack_size) {
- stat_.userspace_cut_stack_samples++;
+ stat_.userspace_truncated_stack_samples++;
}
} else {
stat_.userspace_lost_samples++;
diff --git a/simpleperf/RecordReadThread.h b/simpleperf/RecordReadThread.h
index 658f9ea7..c104b083 100644
--- a/simpleperf/RecordReadThread.h
+++ b/simpleperf/RecordReadThread.h
@@ -93,7 +93,7 @@ struct RecordStat {
size_t kernelspace_lost_records = 0;
size_t userspace_lost_samples = 0;
size_t userspace_lost_non_samples = 0;
- size_t userspace_cut_stack_samples = 0;
+ size_t userspace_truncated_stack_samples = 0;
uint64_t aux_data_size = 0;
uint64_t lost_aux_data_size = 0;
};
@@ -131,8 +131,8 @@ class KernelRecordReader {
class RecordReadThread {
public:
RecordReadThread(size_t record_buffer_size, const perf_event_attr& attr, size_t min_mmap_pages,
- size_t max_mmap_pages, size_t aux_buffer_size, bool allow_cutting_samples = true,
- bool exclude_perf = false);
+ size_t max_mmap_pages, size_t aux_buffer_size,
+ bool allow_truncating_samples = true, bool exclude_perf = false);
~RecordReadThread();
void SetBufferLevels(size_t record_buffer_low_level, size_t record_buffer_critical_level) {
record_buffer_low_level_ = record_buffer_low_level;
diff --git a/simpleperf/RecordReadThread_test.cpp b/simpleperf/RecordReadThread_test.cpp
index 9917c650..e597e443 100644
--- a/simpleperf/RecordReadThread_test.cpp
+++ b/simpleperf/RecordReadThread_test.cpp
@@ -372,7 +372,7 @@ TEST_F(RecordReadThreadTest, process_sample_record) {
ASSERT_FALSE(r);
ASSERT_EQ(thread.GetStat().userspace_lost_samples, 1u);
ASSERT_EQ(thread.GetStat().userspace_lost_non_samples, 0u);
- ASSERT_EQ(thread.GetStat().userspace_cut_stack_samples, 1u);
+ ASSERT_EQ(thread.GetStat().userspace_truncated_stack_samples, 1u);
}
// Test that the data notification exists until the RecordBuffer is empty. So we can read all
@@ -403,7 +403,7 @@ TEST_F(RecordReadThreadTest, has_data_notification_until_buffer_empty) {
ASSERT_TRUE(thread.RemoveEventFds(event_fds));
}
-TEST_F(RecordReadThreadTest, no_cut_samples) {
+TEST_F(RecordReadThreadTest, no_truncated_samples) {
perf_event_attr attr = CreateFakeEventAttr();
attr.sample_type |= PERF_SAMPLE_STACK_USER;
attr.sample_stack_user = 64 * 1024;
@@ -423,7 +423,7 @@ TEST_F(RecordReadThreadTest, no_cut_samples) {
ASSERT_GT(received_samples, 0u);
ASSERT_GT(thread.GetStat().userspace_lost_samples, 0u);
ASSERT_EQ(thread.GetStat().userspace_lost_samples, total_samples - received_samples);
- ASSERT_EQ(thread.GetStat().userspace_cut_stack_samples, 0u);
+ ASSERT_EQ(thread.GetStat().userspace_truncated_stack_samples, 0u);
}
TEST_F(RecordReadThreadTest, exclude_perf) {
diff --git a/simpleperf/cmd_monitor.cpp b/simpleperf/cmd_monitor.cpp
index 2ef9cbca..8e755c12 100644
--- a/simpleperf/cmd_monitor.cpp
+++ b/simpleperf/cmd_monitor.cpp
@@ -239,7 +239,7 @@ bool MonitorCommand::PrepareMonitoring() {
system_wide_collection_ ? kSystemWideRecordBufferSize : kRecordBufferSize;
if (!event_selection_set_.MmapEventFiles(mmap_page_range_.first, mmap_page_range_.second,
0 /* aux_buffer_size */, record_buffer_size,
- false /* allow_cutting_samples */, exclude_perf_)) {
+ false /* allow_truncating_samples */, exclude_perf_)) {
return false;
}
auto callback = std::bind(&MonitorCommand::ProcessRecord, this, std::placeholders::_1);
diff --git a/simpleperf/cmd_record.cpp b/simpleperf/cmd_record.cpp
index 5ef0bd87..d7ffe61d 100644
--- a/simpleperf/cmd_record.cpp
+++ b/simpleperf/cmd_record.cpp
@@ -264,10 +264,10 @@ class RecordCommand : public Command {
" When callchain joiner is used, set the matched nodes needed to join\n"
" callchains. The count should be >= 1. By default it is 1.\n"
"--no-cut-samples Simpleperf uses a record buffer to cache records received from the kernel.\n"
-" When the available space in the buffer reaches low level, it cuts part of\n"
-" the stack data in samples. When the available space reaches critical level,\n"
-" it drops all samples. This option makes simpleperf not cut samples when the\n"
-" available space reaches low level.\n"
+" When the available space in the buffer reaches low level, the stack data in\n"
+" samples is truncated to 1KB. When the available space reaches critical level,\n"
+" it drops all samples. This option makes simpleperf not truncate stack data\n"
+" when the available space reaches low level.\n"
"--keep-failed-unwinding-result Keep reasons for failed unwinding cases\n"
"--keep-failed-unwinding-debug-info Keep debug info for failed unwinding cases\n"
"\n"
@@ -458,7 +458,7 @@ RECORD_FILTER_OPTION_HELP_MSG_FOR_RECORDING
bool allow_callchain_joiner_;
size_t callchain_joiner_min_matching_nodes_;
std::unique_ptr<CallChainJoiner> callchain_joiner_;
- bool allow_cutting_samples_ = true;
+ bool allow_truncating_samples_ = true;
std::unique_ptr<JITDebugReader> jit_debug_reader_;
uint64_t last_record_timestamp_; // used to insert Mmap2Records for JIT debug info
@@ -672,7 +672,7 @@ bool RecordCommand::PrepareRecording(Workload* workload) {
}
if (!event_selection_set_.MmapEventFiles(mmap_page_range_.first, mmap_page_range_.second,
aux_buffer_size_, record_buffer_size,
- allow_cutting_samples_, exclude_perf_)) {
+ allow_truncating_samples_, exclude_perf_)) {
return false;
}
auto callback = std::bind(&RecordCommand::ProcessRecord, this, std::placeholders::_1);
@@ -866,9 +866,9 @@ bool RecordCommand::PostProcessRecording(const std::vector<std::string>& args) {
// 6. Show brief record result.
auto record_stat = event_selection_set_.GetRecordStat();
if (event_selection_set_.HasAuxTrace()) {
- LOG(INFO) << "Aux data traced: " << record_stat.aux_data_size;
+ LOG(INFO) << "Aux data traced: " << ReadableCount(record_stat.aux_data_size);
if (record_stat.lost_aux_data_size != 0) {
- LOG(INFO) << "Aux data lost in user space: " << record_stat.lost_aux_data_size
+ LOG(INFO) << "Aux data lost in user space: " << ReadableCount(record_stat.lost_aux_data_size)
<< ", consider increasing userspace buffer size(--user-buffer-size).";
}
} else {
@@ -879,22 +879,26 @@ bool RecordCommand::PostProcessRecording(const std::vector<std::string>& args) {
size_t lost_samples = record_stat.kernelspace_lost_records + userspace_lost_samples;
std::stringstream os;
- os << "Samples recorded: " << sample_record_count_;
- if (record_stat.userspace_cut_stack_samples > 0) {
- os << " (cut " << record_stat.userspace_cut_stack_samples << ")";
+ os << "Samples recorded: " << ReadableCount(sample_record_count_);
+ if (record_stat.userspace_truncated_stack_samples > 0) {
+ os << " (" << ReadableCount(record_stat.userspace_truncated_stack_samples)
+ << " with truncated stacks)";
}
- os << ". Samples lost: " << lost_samples;
+ os << ". Samples lost: " << ReadableCount(lost_samples);
if (lost_samples != 0) {
- os << " (kernelspace: " << record_stat.kernelspace_lost_records
- << ", userspace: " << userspace_lost_samples << ")";
+ os << " (kernelspace: " << ReadableCount(record_stat.kernelspace_lost_records)
+ << ", userspace: " << ReadableCount(userspace_lost_samples) << ")";
}
os << ".";
LOG(INFO) << os.str();
- LOG(DEBUG) << "Record stat: kernelspace_lost_records=" << record_stat.kernelspace_lost_records
- << ", userspace_lost_samples=" << record_stat.userspace_lost_samples
- << ", userspace_lost_non_samples=" << record_stat.userspace_lost_non_samples
- << ", userspace_cut_stack_samples=" << record_stat.userspace_cut_stack_samples;
+ LOG(DEBUG) << "Record stat: kernelspace_lost_records="
+ << ReadableCount(record_stat.kernelspace_lost_records)
+ << ", userspace_lost_samples=" << ReadableCount(record_stat.userspace_lost_samples)
+ << ", userspace_lost_non_samples="
+ << ReadableCount(record_stat.userspace_lost_non_samples)
+ << ", userspace_truncated_stack_samples="
+ << ReadableCount(record_stat.userspace_truncated_stack_samples);
if (sample_record_count_ + record_stat.kernelspace_lost_records != 0) {
double kernelspace_lost_percent =
@@ -909,16 +913,17 @@ bool RecordCommand::PostProcessRecording(const std::vector<std::string>& args) {
<< "or increasing sample period(-c).";
}
}
- size_t userspace_lost_cut_samples =
- userspace_lost_samples + record_stat.userspace_cut_stack_samples;
+ size_t userspace_lost_truncated_samples =
+ userspace_lost_samples + record_stat.userspace_truncated_stack_samples;
size_t userspace_complete_samples =
- sample_record_count_ - record_stat.userspace_cut_stack_samples;
- if (userspace_complete_samples + userspace_lost_cut_samples != 0) {
- double userspace_lost_percent = static_cast<double>(userspace_lost_cut_samples) /
- (userspace_complete_samples + userspace_lost_cut_samples);
+ sample_record_count_ - record_stat.userspace_truncated_stack_samples;
+ if (userspace_complete_samples + userspace_lost_truncated_samples != 0) {
+ double userspace_lost_percent =
+ static_cast<double>(userspace_lost_truncated_samples) /
+ (userspace_complete_samples + userspace_lost_truncated_samples);
constexpr double USERSPACE_LOST_PERCENT_WARNING_BAR = 0.1;
if (userspace_lost_percent >= USERSPACE_LOST_PERCENT_WARNING_BAR) {
- LOG(WARNING) << "Lost/Cut " << (userspace_lost_percent * 100)
+ LOG(WARNING) << "Lost/Truncated " << (userspace_lost_percent * 100)
<< "% of samples in user space, "
<< "consider increasing userspace buffer size(--user-buffer-size), "
<< "or decreasing sample frequency(-f), "
@@ -1086,7 +1091,7 @@ bool RecordCommand::ParseOptions(const std::vector<std::string>& args,
}
allow_callchain_joiner_ = !options.PullBoolValue("--no-callchain-joiner");
- allow_cutting_samples_ = !options.PullBoolValue("--no-cut-samples");
+ allow_truncating_samples_ = !options.PullBoolValue("--no-cut-samples");
can_dump_kernel_symbols_ = !options.PullBoolValue("--no-dump-kernel-symbols");
dump_symbols_ = !options.PullBoolValue("--no-dump-symbols");
if (auto value = options.PullValue("--no-inherit"); value) {
@@ -2148,10 +2153,10 @@ bool RecordCommand::DumpMetaInfoFeature(bool kernel_symbols_available) {
info_map["record_stat"] = android::base::StringPrintf(
"sample_record_count=%" PRIu64
",kernelspace_lost_records=%zu,userspace_lost_samples=%zu,"
- "userspace_lost_non_samples=%zu,userspace_cut_stack_samples=%zu",
+ "userspace_lost_non_samples=%zu,userspace_truncated_stack_samples=%zu",
sample_record_count_, record_stat.kernelspace_lost_records,
record_stat.userspace_lost_samples, record_stat.userspace_lost_non_samples,
- record_stat.userspace_cut_stack_samples);
+ record_stat.userspace_truncated_stack_samples);
return record_file_writer_->WriteMetaInfoFeature(info_map);
}
diff --git a/simpleperf/cmd_stat.cpp b/simpleperf/cmd_stat.cpp
index 009afc3c..233ff667 100644
--- a/simpleperf/cmd_stat.cpp
+++ b/simpleperf/cmd_stat.cpp
@@ -91,6 +91,18 @@ static const std::unordered_map<std::string_view, std::pair<std::string_view, st
{"raw-l2d-tlb-refill-rd", {"raw-l2d-tlb-rd", "level 2 data TLB refill rate, read"}},
};
+std::string CounterSummary::ReadableCountValue(bool csv) {
+ if (type_name == "cpu-clock" || type_name == "task-clock") {
+ // Convert nanoseconds to milliseconds.
+ double value = count / 1e6;
+ return android::base::StringPrintf("%lf(ms)", value);
+ }
+ if (csv) {
+ return android::base::StringPrintf("%" PRIu64, count);
+ }
+ return ReadableCount(count);
+}
+
const CounterSummary* CounterSummaries::FindSummary(const std::string& type_name,
const std::string& modifier,
const ThreadInfo* thread, int cpu) {
diff --git a/simpleperf/cmd_stat_impl.h b/simpleperf/cmd_stat_impl.h
index 515412bf..bbf165ce 100644
--- a/simpleperf/cmd_stat_impl.h
+++ b/simpleperf/cmd_stat_impl.h
@@ -131,28 +131,7 @@ struct CounterSummary {
}
private:
- std::string ReadableCountValue(bool csv) {
- if (type_name == "cpu-clock" || type_name == "task-clock") {
- // Convert nanoseconds to milliseconds.
- double value = count / 1e6;
- return android::base::StringPrintf("%lf(ms)", value);
- } else {
- // Convert big numbers to human friendly mode. For example,
- // 1000000 will be converted to 1,000,000.
- std::string s = android::base::StringPrintf("%" PRIu64, count);
- if (csv) {
- return s;
- } else {
- for (size_t i = s.size() - 1, j = 1; i > 0; --i, ++j) {
- if (j == 3) {
- s.insert(s.begin() + i, ',');
- j = 0;
- }
- }
- return s;
- }
- }
- }
+ std::string ReadableCountValue(bool csv);
};
BUILD_COMPARE_VALUE_FUNCTION_REVERSE(CompareSummaryCount, count);
diff --git a/simpleperf/doc/README.md b/simpleperf/doc/README.md
index 4555c7c4..8d4040fb 100644
--- a/simpleperf/doc/README.md
+++ b/simpleperf/doc/README.md
@@ -172,18 +172,21 @@ For the missing stack data problem:
well.
2. Simpleperf stores samples in a buffer before unwinding them. If the bufer is low in free space,
- simpleperf may decide to cut stack data for a sample to 1K. Hopefully, this can be recovered by
- callchain joiner. But when a high percentage of samples are cut, many callchains can be broken.
- We can tell if many samples are cut in the record command output, like:
+ simpleperf may decide to truncate stack data for a sample to 1K. Hopefully, this can be recovered
+ by callchain joiner. But when a high percentage of samples are truncated, many callchains can be
+ broken. We can tell if many samples are truncated in the record command output, like:
```sh
$ simpleperf record ...
simpleperf I cmd_record.cpp:809] Samples recorded: 105584 (cut 86291). Samples lost: 6501.
+
+$ simpleperf record ...
+simpleperf I cmd_record.cpp:894] Samples recorded: 7,365 (1,857 with truncated stacks).
```
- There are two ways to avoid cutting samples. One is increasing the buffer size, like
+ There are two ways to avoid truncating samples. One is increasing the buffer size, like
`--user-buffer-size 1G`. But `--user-buffer-size` is only available on latest simpleperf. If that
- option isn't available, we can use `--no-cut-samples` to disable cutting samples.
+ option isn't available, we can use `--no-cut-samples` to disable truncating samples.
For the missing DWARF call frame info problem:
1. Most C++ code generates binaries containing call frame info, in .eh_frame or .ARM.exidx sections.
@@ -268,6 +271,39 @@ disassembly for C++ code and fully compiled Java code. Simpleperf supports two w
2) Use pprof_proto_generator.py to generate pprof proto file. `pprof_proto_generator.py`.
3) Use pprof to report a function with annotated source code, as described [here](https://android.googlesource.com/platform/system/extras/+/main/simpleperf/doc/scripts_reference.md#pprof_proto_generator_py).
+
+### Reduce lost samples and samples with truncated stack
+
+When using `simpleperf record`, we may see lost samples or samples with truncated stack data. Before
+saving samples to a file, simpleperf uses two buffers to cache samples in memory. One is a kernel
+buffer, the other is a userspace buffer. The kernel puts samples to the kernel buffer. Simpleperf
+moves samples from the kernel buffer to the userspace buffer before processing them. If a buffer
+overflows, we lose samples or get samples with truncated stack data. Below is an example.
+
+```sh
+$ simpleperf record -a --duration 1 -g --user-buffer-size 100k
+simpleperf I cmd_record.cpp:799] Recorded for 1.00814 seconds. Start post processing.
+simpleperf I cmd_record.cpp:894] Samples recorded: 79 (16 with truncated stacks).
+ Samples lost: 2,129 (kernelspace: 18, userspace: 2,111).
+simpleperf W cmd_record.cpp:911] Lost 18.5567% of samples in kernel space, consider increasing
+ kernel buffer size(-m), or decreasing sample frequency(-f), or
+ increasing sample period(-c).
+simpleperf W cmd_record.cpp:928] Lost/Truncated 97.1233% of samples in user space, consider
+ increasing userspace buffer size(--user-buffer-size), or
+ decreasing sample frequency(-f), or increasing sample period(-c).
+```
+
+In the above example, we get 79 samples, 16 of them are with truncated stack data. We lose 18
+samples in the kernel buffer, and lose 2111 samples in the userspace buffer.
+
+To reduce lost samples in the kernel buffer, we can increase kernel buffer size via `-m`. To reduce
+lost samples in the userspace buffer, or reduce samples with truncated stack data, we can increase
+userspace buffer size via `--user-buffer-size`.
+
+We can also reduce samples generated in a fixed time period, like reducing sample frequency using
+`-f`, reducing monitored threads, not monitoring multiple perf events at the same time.
+
+
## Bugs and contribution
Bugs and feature requests can be submitted at https://github.com/android/ndk/issues.
diff --git a/simpleperf/event_selection_set.cpp b/simpleperf/event_selection_set.cpp
index 2fdb88f5..ed496589 100644
--- a/simpleperf/event_selection_set.cpp
+++ b/simpleperf/event_selection_set.cpp
@@ -792,10 +792,10 @@ bool EventSelectionSet::ReadCounters(std::vector<CountersInfo>* counters) {
bool EventSelectionSet::MmapEventFiles(size_t min_mmap_pages, size_t max_mmap_pages,
size_t aux_buffer_size, size_t record_buffer_size,
- bool allow_cutting_samples, bool exclude_perf) {
+ bool allow_truncating_samples, bool exclude_perf) {
record_read_thread_.reset(new simpleperf::RecordReadThread(
record_buffer_size, groups_[0][0].event_attr, min_mmap_pages, max_mmap_pages, aux_buffer_size,
- allow_cutting_samples, exclude_perf));
+ allow_truncating_samples, exclude_perf));
return true;
}
diff --git a/simpleperf/event_selection_set.h b/simpleperf/event_selection_set.h
index 757034dc..d203b20c 100644
--- a/simpleperf/event_selection_set.h
+++ b/simpleperf/event_selection_set.h
@@ -164,7 +164,7 @@ class EventSelectionSet {
bool OpenEventFiles(const std::vector<int>& cpus);
bool ReadCounters(std::vector<CountersInfo>* counters);
bool MmapEventFiles(size_t min_mmap_pages, size_t max_mmap_pages, size_t aux_buffer_size,
- size_t record_buffer_size, bool allow_cutting_samples, bool exclude_perf);
+ size_t record_buffer_size, bool allow_truncating_samples, bool exclude_perf);
bool PrepareToReadMmapEventData(const std::function<bool(Record*)>& callback);
bool SyncKernelBuffer();
bool FinishReadMmapEventData();
diff --git a/simpleperf/utils.cpp b/simpleperf/utils.cpp
index 86004677..ac7517e0 100644
--- a/simpleperf/utils.cpp
+++ b/simpleperf/utils.cpp
@@ -498,4 +498,17 @@ void OverflowSafeAdd(uint64_t& dest, uint64_t add) {
}
}
+// Convert big numbers to human friendly mode. For example,
+// 1000000 will be converted to 1,000,000.
+std::string ReadableCount(uint64_t count) {
+ std::string s = std::to_string(count);
+ for (size_t i = s.size() - 1, j = 1; i > 0; --i, ++j) {
+ if (j == 3) {
+ s.insert(s.begin() + i, ',');
+ j = 0;
+ }
+ }
+ return s;
+}
+
} // namespace simpleperf
diff --git a/simpleperf/utils.h b/simpleperf/utils.h
index cda9bbac..0409387f 100644
--- a/simpleperf/utils.h
+++ b/simpleperf/utils.h
@@ -290,6 +290,8 @@ struct OverflowResult {
OverflowResult SafeAdd(uint64_t a, uint64_t b);
void OverflowSafeAdd(uint64_t& dest, uint64_t add);
+std::string ReadableCount(uint64_t count);
+
} // namespace simpleperf
#endif // SIMPLE_PERF_UTILS_H_
diff --git a/simpleperf/utils_test.cpp b/simpleperf/utils_test.cpp
index 2dfbd901..15605338 100644
--- a/simpleperf/utils_test.cpp
+++ b/simpleperf/utils_test.cpp
@@ -105,3 +105,10 @@ TEST(utils, LineReader) {
ASSERT_EQ(*line, "line2");
ASSERT_TRUE(reader.ReadLine() == nullptr);
}
+
+TEST(utils, ReadableCount) {
+ ASSERT_EQ(ReadableCount(0), "0");
+ ASSERT_EQ(ReadableCount(204), "204");
+ ASSERT_EQ(ReadableCount(1000), "1,000");
+ ASSERT_EQ(ReadableCount(123456789), "123,456,789");
+}