diff options
author | Yabin Cui <yabinc@google.com> | 2023-09-01 23:57:26 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2023-09-01 23:57:26 +0000 |
commit | 0f0aecc178b6f01223d14b06206a766b56aa55e1 (patch) | |
tree | 2435868c60ecb911fd07cd11abfb0431cfe23d1f | |
parent | 8cf8a27b6994e962be9968fc0e5ec9a02e20840c (diff) | |
parent | 1be9007443caa267f1158e0de05b89e6626ea3ab (diff) | |
download | extras-0f0aecc178b6f01223d14b06206a766b56aa55e1.tar.gz |
Merge changes Ie7cf3774,Ia3cbaf04,Ide7496a8 into main
* changes:
simpleperf: Add doc for reducing lost/truncated samples
simpleperf: Use thousand groups when reporting sample counts
simpleperf: Improve log message
-rw-r--r-- | simpleperf/RecordReadThread.cpp | 8 | ||||
-rw-r--r-- | simpleperf/RecordReadThread.h | 6 | ||||
-rw-r--r-- | simpleperf/RecordReadThread_test.cpp | 6 | ||||
-rw-r--r-- | simpleperf/cmd_monitor.cpp | 2 | ||||
-rw-r--r-- | simpleperf/cmd_record.cpp | 61 | ||||
-rw-r--r-- | simpleperf/cmd_stat.cpp | 12 | ||||
-rw-r--r-- | simpleperf/cmd_stat_impl.h | 23 | ||||
-rw-r--r-- | simpleperf/doc/README.md | 46 | ||||
-rw-r--r-- | simpleperf/event_selection_set.cpp | 4 | ||||
-rw-r--r-- | simpleperf/event_selection_set.h | 2 | ||||
-rw-r--r-- | simpleperf/utils.cpp | 13 | ||||
-rw-r--r-- | simpleperf/utils.h | 2 | ||||
-rw-r--r-- | simpleperf/utils_test.cpp | 7 |
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"); +} |