[cpu-profiler] Implement CPU profiler subsampling/multiplexing

Permit individual calls to CpuProfiler::StartSampling to provide their
own requested sampling interval, to be snapped to the profiler's
sampling interval. Use the greatest common divisor of all sample rates
to determine what sample rate should be chosen for the sampling thread,
and dispatch samples to attached profilers based on their requested
sample periodicity.

Change-Id: I0b076d09761d7176f31725e112578b68ab5da54c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1484461
Reviewed-by: Ulan Degenbaev <ulan@chromium.org>
Reviewed-by: Alexei Filippov <alph@chromium.org>
Reviewed-by: Peter Marshall <petermarshall@chromium.org>
Commit-Queue: Andrew Comminos <acomminos@fb.com>
Cr-Commit-Position: refs/heads/master@{#61548}
This commit is contained in:
Andrew Comminos 2019-05-14 13:24:49 -07:00 committed by Commit Bot
parent a7985022f6
commit deb3231a23
8 changed files with 313 additions and 19 deletions

View File

@ -321,22 +321,33 @@ class V8_EXPORT CpuProfilingOptions {
* \param max_samples The maximum number of samples that should be recorded by
* the profiler. Samples obtained after this limit will be
* discarded.
* \param sampling_interval_us controls the profile-specific target
* sampling interval. The provided sampling
* interval will be snapped to the next lowest
* non-zero multiple of the profiler's sampling
* interval, set via SetSamplingInterval(). If
* zero, the sampling interval will be equal to
* the profiler's sampling interval.
*/
CpuProfilingOptions(CpuProfilingMode mode = kLeafNodeLineNumbers,
bool record_samples = false,
unsigned max_samples = kNoSampleLimit)
unsigned max_samples = kNoSampleLimit,
int sampling_interval_us = 0)
: mode_(mode),
record_samples_(record_samples),
max_samples_(max_samples) {}
max_samples_(max_samples),
sampling_interval_us_(sampling_interval_us) {}
CpuProfilingMode mode() const { return mode_; }
bool record_samples() const { return record_samples_; }
unsigned max_samples() const { return max_samples_; }
int sampling_interval_us() const { return sampling_interval_us_; }
private:
CpuProfilingMode mode_;
bool record_samples_;
unsigned max_samples_;
int sampling_interval_us_;
};
/**

View File

@ -32,7 +32,9 @@ class CpuSampler : public sampler::Sampler {
TickSample* sample = processor_->StartTickSample();
if (sample == nullptr) return;
Isolate* isolate = reinterpret_cast<Isolate*>(this->isolate());
sample->Init(isolate, regs, TickSample::kIncludeCEntryFrame, true);
sample->Init(isolate, regs, TickSample::kIncludeCEntryFrame,
/* update_stats */ true,
/* use_simulator_reg_state */ true, processor_->period());
if (is_counting_samples_ && !sample->timestamp.IsNull()) {
if (sample->state == JS) ++js_sample_count_;
if (sample->state == EXTERNAL) ++external_sample_count_;
@ -243,6 +245,16 @@ void SamplingEventsProcessor::Run() {
} while (ProcessCodeEvent());
}
void SamplingEventsProcessor::SetSamplingInterval(base::TimeDelta period) {
if (period_ == period) return;
StopSynchronously();
period_ = period;
base::Relaxed_Store(&running_, 1);
StartSynchronously();
}
void* SamplingEventsProcessor::operator new(size_t size) {
return AlignedAlloc(size, alignof(SamplingEventsProcessor));
}
@ -321,7 +333,7 @@ CpuProfiler::CpuProfiler(Isolate* isolate, CpuProfilingNamingMode naming_mode,
ProfilerEventsProcessor* test_processor)
: isolate_(isolate),
naming_mode_(naming_mode),
sampling_interval_(base::TimeDelta::FromMicroseconds(
base_sampling_interval_(base::TimeDelta::FromMicroseconds(
FLAG_cpu_profiler_sampling_interval)),
profiles_(test_profiles),
generator_(test_generator),
@ -338,7 +350,7 @@ CpuProfiler::~CpuProfiler() {
void CpuProfiler::set_sampling_interval(base::TimeDelta value) {
DCHECK(!is_profiling_);
sampling_interval_ = value;
base_sampling_interval_ = value;
}
void CpuProfiler::set_use_precise_sampling(bool value) {
@ -365,6 +377,17 @@ void CpuProfiler::CreateEntriesForRuntimeCallStats() {
}
}
base::TimeDelta CpuProfiler::ComputeSamplingInterval() const {
return profiles_->GetCommonSamplingInterval();
}
void CpuProfiler::AdjustSamplingInterval() {
if (!processor_) return;
base::TimeDelta base_interval = ComputeSamplingInterval();
processor_->SetSamplingInterval(base_interval);
}
// static
void CpuProfiler::CollectSample(Isolate* isolate) {
GetProfilersManager()->CallCollectSample(isolate);
@ -380,6 +403,7 @@ void CpuProfiler::StartProfiling(const char* title,
CpuProfilingOptions options) {
if (profiles_->StartProfiling(title, options)) {
TRACE_EVENT0("v8", "CpuProfiler::StartProfiling");
AdjustSamplingInterval();
StartProcessorIfNotStarted();
}
}
@ -402,8 +426,9 @@ void CpuProfiler::StartProcessorIfNotStarted() {
codemap_needs_initialization = true;
CreateEntriesForRuntimeCallStats();
}
base::TimeDelta sampling_interval = ComputeSamplingInterval();
processor_.reset(new SamplingEventsProcessor(
isolate_, generator_.get(), sampling_interval_, use_precise_sampling_));
isolate_, generator_.get(), sampling_interval, use_precise_sampling_));
if (profiler_listener_) {
profiler_listener_->set_observer(processor_.get());
} else {
@ -430,7 +455,9 @@ void CpuProfiler::StartProcessorIfNotStarted() {
CpuProfile* CpuProfiler::StopProfiling(const char* title) {
if (!is_profiling_) return nullptr;
StopProcessorIfLastProfile(title);
return profiles_->StopProfiling(title);
CpuProfile* result = profiles_->StopProfiling(title);
AdjustSamplingInterval();
return result;
}
CpuProfile* CpuProfiler::StopProfiling(String title) {

View File

@ -172,6 +172,8 @@ class V8_EXPORT_PRIVATE ProfilerEventsProcessor : public base::Thread,
// Add a sample into the tick sample events buffer. Used for testing.
void AddSample(TickSample sample);
virtual void SetSamplingInterval(base::TimeDelta) {}
protected:
ProfilerEventsProcessor(Isolate* isolate, ProfileGenerator* generator);
@ -211,6 +213,8 @@ class V8_EXPORT_PRIVATE SamplingEventsProcessor
void Run() override;
void SetSamplingInterval(base::TimeDelta period) override;
// Tick sample events are filled directly in the buffer of the circular
// queue (because the structure is of fixed width, but usually not all
// stack frame entries are filled.) This method returns a pointer to the
@ -221,6 +225,7 @@ class V8_EXPORT_PRIVATE SamplingEventsProcessor
inline void FinishTickSample();
sampler::Sampler* sampler() { return sampler_.get(); }
base::TimeDelta period() const { return period_; }
private:
SampleProcessingResult ProcessOneSample() override;
@ -231,7 +236,7 @@ class V8_EXPORT_PRIVATE SamplingEventsProcessor
SamplingCircularQueue<TickSampleEventRecord,
kTickSampleQueueLength> ticks_buffer_;
std::unique_ptr<sampler::Sampler> sampler_;
const base::TimeDelta period_; // Samples & code events processing period.
base::TimeDelta period_; // Samples & code events processing period.
const bool use_precise_sampling_; // Whether or not busy-waiting is used for
// low sampling intervals on Windows.
};
@ -251,6 +256,7 @@ class V8_EXPORT_PRIVATE CpuProfiler {
typedef v8::CpuProfilingMode ProfilingMode;
typedef v8::CpuProfilingNamingMode NamingMode;
base::TimeDelta sampling_interval() const { return base_sampling_interval_; }
void set_sampling_interval(base::TimeDelta value);
void set_use_precise_sampling(bool);
void CollectSample();
@ -281,10 +287,18 @@ class V8_EXPORT_PRIVATE CpuProfiler {
void LogBuiltins();
void CreateEntriesForRuntimeCallStats();
// Computes a sampling interval sufficient to accomodate attached profiles.
base::TimeDelta ComputeSamplingInterval() const;
// Dynamically updates the sampler to use a sampling interval sufficient for
// child profiles.
void AdjustSamplingInterval();
Isolate* const isolate_;
const NamingMode naming_mode_;
base::TimeDelta sampling_interval_;
bool use_precise_sampling_ = true;
// Sampling interval to which per-profile sampling intervals will be clamped
// to a multiple of, or used as the default if unspecified.
base::TimeDelta base_sampling_interval_;
std::unique_ptr<CpuProfilesCollection> profiles_;
std::unique_ptr<ProfileGenerator> generator_;
std::unique_ptr<ProfilerEventsProcessor> processor_;

View File

@ -4,6 +4,8 @@
#include "src/profiler/profile-generator.h"
#include <algorithm>
#include "src/objects/shared-function-info-inl.h"
#include "src/profiler/cpu-profiler.h"
#include "src/profiler/profile-generator-inl.h"
@ -488,9 +490,28 @@ CpuProfile::CpuProfile(CpuProfiler* profiler, const char* title,
"Profile", id_, "data", std::move(value));
}
bool CpuProfile::CheckSubsample(base::TimeDelta source_sampling_interval) {
DCHECK_GE(source_sampling_interval, base::TimeDelta());
// If the sampling source's sampling interval is 0, record as many samples
// are possible irrespective of the profile's sampling interval. Manually
// taken samples (via CollectSample) fall into this case as well.
if (source_sampling_interval.IsZero()) return true;
next_sample_delta_ -= source_sampling_interval;
if (next_sample_delta_ <= base::TimeDelta()) {
next_sample_delta_ =
base::TimeDelta::FromMicroseconds(options_.sampling_interval_us());
return true;
}
return false;
}
void CpuProfile::AddPath(base::TimeTicks timestamp,
const ProfileStackTrace& path, int src_line,
bool update_stats) {
bool update_stats, base::TimeDelta sampling_interval) {
if (!CheckSubsample(sampling_interval)) return;
ProfileNode* top_frame_node =
top_down_.AddPathFromEnd(path, src_line, update_stats, options_.mode());
@ -760,15 +781,46 @@ void CpuProfilesCollection::RemoveProfile(CpuProfile* profile) {
finished_profiles_.erase(pos);
}
namespace {
int64_t GreatestCommonDivisor(int64_t a, int64_t b) {
return b ? GreatestCommonDivisor(b, a % b) : a;
}
} // namespace
base::TimeDelta CpuProfilesCollection::GetCommonSamplingInterval() const {
DCHECK(profiler_);
int64_t base_sampling_interval_us =
profiler_->sampling_interval().InMicroseconds();
if (base_sampling_interval_us == 0) return base::TimeDelta();
int64_t interval_us = 0;
for (const auto& profile : current_profiles_) {
// Snap the profile's requested sampling interval to the next multiple of
// the base sampling interval.
int64_t profile_interval_us =
std::max<int64_t>(
(profile->sampling_interval_us() + base_sampling_interval_us - 1) /
base_sampling_interval_us,
1) *
base_sampling_interval_us;
interval_us = GreatestCommonDivisor(interval_us, profile_interval_us);
}
return base::TimeDelta::FromMicroseconds(interval_us);
}
void CpuProfilesCollection::AddPathToCurrentProfiles(
base::TimeTicks timestamp, const ProfileStackTrace& path, int src_line,
bool update_stats) {
bool update_stats, base::TimeDelta sampling_interval) {
// As starting / stopping profiles is rare relatively to this
// method, we don't bother minimizing the duration of lock holding,
// e.g. copying contents of the list to a local vector.
current_profiles_semaphore_.Wait();
for (const std::unique_ptr<CpuProfile>& profile : current_profiles_) {
profile->AddPath(timestamp, path, src_line, update_stats);
profile->AddPath(timestamp, path, src_line, update_stats,
sampling_interval);
}
current_profiles_semaphore_.Signal();
}
@ -902,7 +954,8 @@ void ProfileGenerator::RecordTickSample(const TickSample& sample) {
}
profiles_->AddPathToCurrentProfiles(sample.timestamp, stack_trace, src_line,
sample.update_stats);
sample.update_stats,
sample.sampling_interval);
}
CodeEntry* ProfileGenerator::EntryForVMState(StateTag tag) {

View File

@ -364,12 +364,16 @@ class CpuProfile {
int line;
};
CpuProfile(CpuProfiler* profiler, const char* title,
CpuProfilingOptions options);
V8_EXPORT_PRIVATE CpuProfile(CpuProfiler* profiler, const char* title,
CpuProfilingOptions options);
// Checks whether or not the given TickSample should be (sub)sampled, given
// the sampling interval of the profiler that recorded it (in microseconds).
V8_EXPORT_PRIVATE bool CheckSubsample(base::TimeDelta sampling_interval);
// Add pc -> ... -> main() call path to the profile.
void AddPath(base::TimeTicks timestamp, const ProfileStackTrace& path,
int src_line, bool update_stats);
int src_line, bool update_stats,
base::TimeDelta sampling_interval);
void FinishProfile();
const char* title() const { return title_; }
@ -378,6 +382,10 @@ class CpuProfile {
int samples_count() const { return static_cast<int>(samples_.size()); }
const SampleInfo& sample(int index) const { return samples_[index]; }
int64_t sampling_interval_us() const {
return options_.sampling_interval_us();
}
base::TimeTicks start_time() const { return start_time_; }
base::TimeTicks end_time() const { return end_time_; }
CpuProfiler* cpu_profiler() const { return profiler_; }
@ -398,6 +406,9 @@ class CpuProfile {
CpuProfiler* const profiler_;
size_t streaming_next_sample_;
uint32_t id_;
// Number of microseconds worth of profiler ticks that should elapse before
// the next sample is recorded.
base::TimeDelta next_sample_delta_;
static std::atomic<uint32_t> last_id_;
@ -446,6 +457,7 @@ class V8_EXPORT_PRIVATE CpuProfilesCollection {
void set_cpu_profiler(CpuProfiler* profiler) { profiler_ = profiler; }
bool StartProfiling(const char* title, CpuProfilingOptions options = {});
CpuProfile* StopProfiling(const char* title);
std::vector<std::unique_ptr<CpuProfile>>* profiles() {
return &finished_profiles_;
@ -454,10 +466,16 @@ class V8_EXPORT_PRIVATE CpuProfilesCollection {
bool IsLastProfile(const char* title);
void RemoveProfile(CpuProfile* profile);
// Finds a common sampling interval dividing each CpuProfile's interval,
// rounded up to the nearest multiple of the CpuProfiler's sampling interval.
// Returns 0 if no profiles are attached.
base::TimeDelta GetCommonSamplingInterval() const;
// Called from profile generator thread.
void AddPathToCurrentProfiles(base::TimeTicks timestamp,
const ProfileStackTrace& path, int src_line,
bool update_stats);
bool update_stats,
base::TimeDelta sampling_interval);
// Limits the number of profiles that can be simultaneously collected.
static const int kMaxSimultaneousProfiles = 100;

View File

@ -292,10 +292,12 @@ namespace internal {
void TickSample::Init(Isolate* isolate, const v8::RegisterState& state,
RecordCEntryFrame record_c_entry_frame, bool update_stats,
bool use_simulator_reg_state) {
bool use_simulator_reg_state,
base::TimeDelta sampling_interval) {
v8::TickSample::Init(reinterpret_cast<v8::Isolate*>(isolate), state,
record_c_entry_frame, update_stats,
use_simulator_reg_state);
this->sampling_interval = sampling_interval;
if (pc == nullptr) return;
timestamp = base::TimeTicks::HighResolutionNow();
}
@ -312,6 +314,8 @@ void TickSample::print() const {
PrintF(" - %s: %p\n",
has_external_callback ? "external_callback_entry" : "tos", tos);
PrintF(" - update_stats: %d\n", update_stats);
PrintF(" - sampling_interval: %" PRId64 "\n",
sampling_interval.InMicroseconds());
PrintF("\n");
}

View File

@ -17,8 +17,10 @@ class Isolate;
struct TickSample : public v8::TickSample {
void Init(Isolate* isolate, const v8::RegisterState& state,
RecordCEntryFrame record_c_entry_frame, bool update_stats,
bool use_simulator_reg_state = true);
bool use_simulator_reg_state = true,
base::TimeDelta sampling_interval = base::TimeDelta());
base::TimeTicks timestamp;
base::TimeDelta sampling_interval; // Sampling interval used to capture.
void print() const;
};

View File

@ -3171,6 +3171,171 @@ TEST(SampleLimit) {
CHECK_EQ(profile->GetSamplesCount(), 50);
}
// Tests that a CpuProfile instance subsamples from a stream of tick samples
// appropriately.
TEST(ProflilerSubsampling) {
LocalContext env;
i::Isolate* isolate = CcTest::i_isolate();
i::HandleScope scope(isolate);
CpuProfilesCollection* profiles = new CpuProfilesCollection(isolate);
ProfileGenerator* generator = new ProfileGenerator(profiles);
ProfilerEventsProcessor* processor = new SamplingEventsProcessor(
isolate, generator, v8::base::TimeDelta::FromMicroseconds(1),
/* use_precise_sampling */ true);
CpuProfiler profiler(isolate, kDebugNaming, profiles, generator, processor);
// Create a new CpuProfile that wants samples at 8us.
CpuProfile profile(&profiler, "",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 8});
// Verify that the first sample is always included.
CHECK(profile.CheckSubsample(base::TimeDelta::FromMicroseconds(10)));
// 4 2us samples should result in one 8us sample.
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
// Profiles should expect the source sample interval to change, in which case
// they should still take the first sample elapsed after their interval.
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(!profile.CheckSubsample(base::TimeDelta::FromMicroseconds(2)));
CHECK(profile.CheckSubsample(base::TimeDelta::FromMicroseconds(4)));
// Aligned samples (at 8us) are always included.
CHECK(profile.CheckSubsample(base::TimeDelta::FromMicroseconds(8)));
// Samples with a rate of 0 should always be included.
CHECK(profile.CheckSubsample(base::TimeDelta::FromMicroseconds(0)));
}
// Tests that the base sampling rate of a CpuProfilesCollection is dynamically
// chosen based on the GCD of its child profiles.
TEST(DynamicResampling) {
LocalContext env;
i::Isolate* isolate = CcTest::i_isolate();
i::HandleScope scope(isolate);
CpuProfilesCollection* profiles = new CpuProfilesCollection(isolate);
ProfileGenerator* generator = new ProfileGenerator(profiles);
ProfilerEventsProcessor* processor = new SamplingEventsProcessor(
isolate, generator, v8::base::TimeDelta::FromMicroseconds(1),
/* use_precise_sampling */ true);
CpuProfiler profiler(isolate, kDebugNaming, profiles, generator, processor);
// Set a 1us base sampling rate, dividing all possible intervals.
profiler.set_sampling_interval(base::TimeDelta::FromMicroseconds(1));
// Verify that the sampling interval with no started profilers is unset.
CHECK_EQ(profiles->GetCommonSamplingInterval(), base::TimeDelta());
// Add a 10us profiler, verify that the base sampling interval is as high as
// possible (10us).
profiles->StartProfiling("10us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 10});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(10));
// Add a 5us profiler, verify that the base sampling interval is as high as
// possible given a 10us and 5us profiler (5us).
profiles->StartProfiling("5us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 5});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(5));
// Add a 3us profiler, verify that the base sampling interval is 1us (due to
// coprime intervals).
profiles->StartProfiling("3us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 3});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(1));
// Remove the 5us profiler, verify that the sample interval stays at 1us.
profiles->StopProfiling("5us");
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(1));
// Remove the 10us profiler, verify that the sample interval becomes 3us.
profiles->StopProfiling("10us");
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(3));
// Remove the 3us profiler, verify that the sample interval becomes unset.
profiles->StopProfiling("3us");
CHECK_EQ(profiles->GetCommonSamplingInterval(), base::TimeDelta());
}
// Ensures that when a non-unit base sampling interval is set on the profiler,
// that the sampling rate gets snapped to the nearest multiple prior to GCD
// computation.
TEST(DynamicResamplingWithBaseInterval) {
LocalContext env;
i::Isolate* isolate = CcTest::i_isolate();
i::HandleScope scope(isolate);
CpuProfilesCollection* profiles = new CpuProfilesCollection(isolate);
ProfileGenerator* generator = new ProfileGenerator(profiles);
ProfilerEventsProcessor* processor = new SamplingEventsProcessor(
isolate, generator, v8::base::TimeDelta::FromMicroseconds(1),
/* use_precise_sampling */ true);
CpuProfiler profiler(isolate, kDebugNaming, profiles, generator, processor);
profiler.set_sampling_interval(base::TimeDelta::FromMicroseconds(7));
// Verify that the sampling interval with no started profilers is unset.
CHECK_EQ(profiles->GetCommonSamplingInterval(), base::TimeDelta());
// Add a profiler with an unset sampling interval, verify that the common
// sampling interval is equal to the base.
profiles->StartProfiling("unset",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(7));
profiles->StopProfiling("unset");
// Adding a 8us sampling interval rounds to a 14us base interval.
profiles->StartProfiling("8us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 8});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(14));
// Adding a 4us sampling interval should cause a lowering to a 7us interval.
profiles->StartProfiling("4us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 4});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(7));
// Removing the 4us sampling interval should restore the 14us sampling
// interval.
profiles->StopProfiling("4us");
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(14));
// Removing the 8us sampling interval should unset the common sampling
// interval.
profiles->StopProfiling("8us");
CHECK_EQ(profiles->GetCommonSamplingInterval(), base::TimeDelta());
// A sampling interval of 0us should enforce all profiles to have a sampling
// interval of 0us (the only multiple of 0).
profiler.set_sampling_interval(base::TimeDelta::FromMicroseconds(0));
profiles->StartProfiling("5us",
{v8::CpuProfilingMode::kLeafNodeLineNumbers, true,
v8::CpuProfilingOptions::kNoSampleLimit, 5});
CHECK_EQ(profiles->GetCommonSamplingInterval(),
base::TimeDelta::FromMicroseconds(0));
profiles->StopProfiling("5us");
}
enum class EntryCountMode { kAll, kOnlyInlined };
// Count the number of unique source positions.