Metrics provider for statistical stack profiler

Provides a metrics provider that uploads statistical stack profiling
state via UMA.

This CL builds on top of the statistical profiler implementation in
https://crrev.com/1016563004, which is under review.

BUG=464929

Review URL: https://codereview.chromium.org/981143006

Cr-Commit-Position: refs/heads/master@{#321928}
diff --git a/base/profiler/stack_sampling_profiler.cc b/base/profiler/stack_sampling_profiler.cc
index 57b7b35..f760507 100644
--- a/base/profiler/stack_sampling_profiler.cc
+++ b/base/profiler/stack_sampling_profiler.cc
@@ -65,6 +65,10 @@
 }  // namespace
 
 StackSamplingProfiler::Module::Module() : base_address(nullptr) {}
+StackSamplingProfiler::Module::Module(const void* base_address,
+                                      const std::string& id,
+                                      const FilePath& filename)
+    : base_address(base_address), id(id), filename(filename) {}
 
 StackSamplingProfiler::Module::~Module() {}
 
@@ -72,6 +76,11 @@
     : instruction_pointer(nullptr),
       module_index(-1) {}
 
+StackSamplingProfiler::Frame::Frame(const void* instruction_pointer,
+                                    int module_index)
+    : instruction_pointer(instruction_pointer),
+      module_index(module_index) {}
+
 StackSamplingProfiler::Frame::~Frame() {}
 
 StackSamplingProfiler::Profile::Profile() : preserve_sample_ordering(false) {}
diff --git a/base/profiler/stack_sampling_profiler.h b/base/profiler/stack_sampling_profiler.h
index 8d7671e..4438786 100644
--- a/base/profiler/stack_sampling_profiler.h
+++ b/base/profiler/stack_sampling_profiler.h
@@ -53,6 +53,8 @@
   // Module represents the module (DLL or exe) corresponding to a stack frame.
   struct BASE_EXPORT Module {
     Module();
+    Module(const void* base_address, const std::string& id,
+           const FilePath& filename);
     ~Module();
 
     // Points to the base address of the module.
@@ -72,6 +74,7 @@
   // Frame represents an individual sampled stack frame with module information.
   struct BASE_EXPORT Frame {
     Frame();
+    Frame(const void* instruction_pointer, int module_index);
     ~Frame();
 
     // The sampled instruction pointer within the function.
diff --git a/chrome/browser/metrics/chrome_metrics_service_client.cc b/chrome/browser/metrics/chrome_metrics_service_client.cc
index 6aa71c8..2082442 100644
--- a/chrome/browser/metrics/chrome_metrics_service_client.cc
+++ b/chrome/browser/metrics/chrome_metrics_service_client.cc
@@ -32,6 +32,7 @@
 #include "chrome/common/crash_keys.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/common/render_messages.h"
+#include "components/metrics/call_stack_profile_metrics_provider.h"
 #include "components/metrics/gpu/gpu_metrics_provider.h"
 #include "components/metrics/metrics_service.h"
 #include "components/metrics/net/net_metrics_log_uploader.h"
@@ -334,6 +335,10 @@
   metrics_service_->RegisterMetricsProvider(
       scoped_ptr<metrics::MetricsProvider>(profiler_metrics_provider_));
 
+  metrics_service_->RegisterMetricsProvider(
+      scoped_ptr<metrics::MetricsProvider>(
+          new metrics::CallStackProfileMetricsProvider));
+
 #if defined(OS_ANDROID)
   metrics_service_->RegisterMetricsProvider(
       scoped_ptr<metrics::MetricsProvider>(
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index a3831445..df6bbf1a 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -243,6 +243,7 @@
       'login/screens/screen_context_unittest.cc',
     ],
     'metrics_unittest_sources': [
+      'metrics/call_stack_profile_metrics_provider_unittest.cc',
       'metrics/compression_utils_unittest.cc',
       'metrics/daily_event_unittest.cc',
       'metrics/gpu/gpu_metrics_provider_unittest.cc',
diff --git a/components/metrics.gypi b/components/metrics.gypi
index 9a8f19de..f0d3f41 100644
--- a/components/metrics.gypi
+++ b/components/metrics.gypi
@@ -23,6 +23,8 @@
         'component_metrics_proto',
       ],
       'sources': [
+        'metrics/call_stack_profile_metrics_provider.cc',
+        'metrics/call_stack_profile_metrics_provider.h',
         'metrics/clean_exit_beacon.cc',
         'metrics/clean_exit_beacon.h',
         'metrics/client_info.cc',
@@ -158,6 +160,7 @@
       'target_name': 'component_metrics_proto',
       'type': 'static_library',
       'sources': [
+        'metrics/proto/call_stack_profile.proto',
         'metrics/proto/cast_logs.proto',
         'metrics/proto/chrome_user_metrics_extension.proto',
         'metrics/proto/histogram_event.proto',
diff --git a/components/metrics/BUILD.gn b/components/metrics/BUILD.gn
index 7f4b9b5..c5ae0c7 100644
--- a/components/metrics/BUILD.gn
+++ b/components/metrics/BUILD.gn
@@ -5,6 +5,8 @@
 # GYP version: components/metrics.gypi:metrics
 source_set("metrics") {
   sources = [
+    "call_stack_profile_metrics_provider.cc",
+    "call_stack_profile_metrics_provider.h",
     "clean_exit_beacon.cc",
     "clean_exit_beacon.h",
     "client_info.cc",
diff --git a/components/metrics/call_stack_profile_metrics_provider.cc b/components/metrics/call_stack_profile_metrics_provider.cc
new file mode 100644
index 0000000..7ed60f5
--- /dev/null
+++ b/components/metrics/call_stack_profile_metrics_provider.cc
@@ -0,0 +1,133 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/metrics/call_stack_profile_metrics_provider.h"
+
+#include <cstring>
+#include <map>
+#include <utility>
+
+#include "base/logging.h"
+#include "base/macros.h"
+#include "base/profiler/stack_sampling_profiler.h"
+#include "components/metrics/metrics_hashes.h"
+#include "components/metrics/proto/chrome_user_metrics_extension.pb.h"
+
+using base::StackSamplingProfiler;
+
+namespace metrics {
+
+namespace {
+
+// The protobuf expects the MD5 checksum prefix of the module name.
+uint64 HashModuleFilename(const base::FilePath& filename) {
+  const base::FilePath::StringType basename = filename.BaseName().value();
+  // Copy the bytes in basename into a string buffer.
+  size_t basename_length_in_bytes =
+      basename.size() * sizeof(base::FilePath::CharType);
+  std::string name_bytes(basename_length_in_bytes, '\0');
+  memcpy(&name_bytes[0], &basename[0], basename_length_in_bytes);
+  return HashMetricName(name_bytes);
+}
+
+// Transcode |sample| into |proto_sample|, using base addresses in |modules| to
+// compute module instruction pointer offsets.
+void CopySampleToProto(
+    const StackSamplingProfiler::Sample& sample,
+    const std::vector<StackSamplingProfiler::Module>& modules,
+    CallStackProfile::Sample* proto_sample) {
+  for (const StackSamplingProfiler::Frame& frame : sample) {
+    CallStackProfile::Entry* entry = proto_sample->add_entry();
+    // A frame may not have a valid module. If so, we can't compute the
+    // instruction pointer offset, and we don't want to send bare pointers, so
+    // leave call_stack_entry empty.
+    if (frame.module_index < 0)
+      continue;
+    int64 module_offset =
+        reinterpret_cast<const char*>(frame.instruction_pointer) -
+        reinterpret_cast<const char*>(modules[frame.module_index].base_address);
+    DCHECK_GE(module_offset, 0);
+    entry->set_address(static_cast<uint64>(module_offset));
+    entry->set_module_id_index(frame.module_index);
+  }
+}
+
+// Transcode |profile| into |proto_profile|.
+void CopyProfileToProto(
+    const StackSamplingProfiler::Profile& profile,
+    CallStackProfile* proto_profile) {
+  if (profile.samples.empty())
+    return;
+
+  if (profile.preserve_sample_ordering) {
+    // Collapse only consecutive repeated samples together.
+    CallStackProfile::Sample* current_sample_proto = nullptr;
+    for (auto it = profile.samples.begin(); it != profile.samples.end(); ++it) {
+      if (!current_sample_proto || *it != *(it - 1)) {
+        current_sample_proto = proto_profile->add_sample();
+        CopySampleToProto(*it, profile.modules, current_sample_proto);
+        current_sample_proto->set_count(1);
+      } else {
+        current_sample_proto->set_count(current_sample_proto->count() + 1);
+      }
+    }
+  } else {
+    // Collapse all repeated samples together.
+    std::map<StackSamplingProfiler::Sample, int> sample_index;
+    for (auto it = profile.samples.begin(); it != profile.samples.end(); ++it) {
+      auto location = sample_index.find(*it);
+      if (location == sample_index.end()) {
+        CallStackProfile::Sample* sample_proto = proto_profile->add_sample();
+        CopySampleToProto(*it, profile.modules, sample_proto);
+        sample_proto->set_count(1);
+        sample_index.insert(
+            std::make_pair(
+                *it, static_cast<int>(proto_profile->sample().size()) - 1));
+      } else {
+        CallStackProfile::Sample* sample_proto =
+            proto_profile->mutable_sample()->Mutable(location->second);
+        sample_proto->set_count(sample_proto->count() + 1);
+      }
+    }
+  }
+
+  for (const StackSamplingProfiler::Module& module : profile.modules) {
+    CallStackProfile::ModuleIdentifier* module_id =
+        proto_profile->add_module_id();
+    module_id->set_build_id(module.id);
+    module_id->set_name_md5_prefix(HashModuleFilename(module.filename));
+  }
+
+  proto_profile->set_profile_duration_ms(
+      profile.profile_duration.InMilliseconds());
+  proto_profile->set_sampling_period_ms(
+      profile.sampling_period.InMilliseconds());
+}
+}  // namespace
+
+CallStackProfileMetricsProvider::CallStackProfileMetricsProvider() {}
+
+CallStackProfileMetricsProvider::~CallStackProfileMetricsProvider() {}
+
+void CallStackProfileMetricsProvider::ProvideGeneralMetrics(
+    ChromeUserMetricsExtension* uma_proto) {
+  std::vector<StackSamplingProfiler::Profile> profiles;
+  if (!source_profiles_for_test_.empty())
+    profiles.swap(source_profiles_for_test_);
+  else
+    StackSamplingProfiler::GetPendingProfiles(&profiles);
+
+  for (const StackSamplingProfiler::Profile& profile : profiles) {
+    CallStackProfile* call_stack_profile =
+        uma_proto->add_sampled_profile()->mutable_call_stack_profile();
+    CopyProfileToProto(profile, call_stack_profile);
+  }
+}
+
+void CallStackProfileMetricsProvider::SetSourceProfilesForTesting(
+    const std::vector<StackSamplingProfiler::Profile>& profiles) {
+  source_profiles_for_test_ = profiles;
+}
+
+}  // namespace metrics
diff --git a/components/metrics/call_stack_profile_metrics_provider.h b/components/metrics/call_stack_profile_metrics_provider.h
new file mode 100644
index 0000000..a2bb0f8
--- /dev/null
+++ b/components/metrics/call_stack_profile_metrics_provider.h
@@ -0,0 +1,39 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_METRICS_CALL_STACK_PROFILE_METRICS_PROVIDER_H_
+#define COMPONENTS_METRICS_CALL_STACK_PROFILE_METRICS_PROVIDER_H_
+
+#include <vector>
+
+#include "base/profiler/stack_sampling_profiler.h"
+#include "components/metrics/metrics_provider.h"
+
+namespace metrics {
+class ChromeUserMetricsExtension;
+
+// Performs metrics logging for the stack sampling profiler.
+class CallStackProfileMetricsProvider : public MetricsProvider {
+ public:
+  CallStackProfileMetricsProvider();
+  ~CallStackProfileMetricsProvider() override;
+
+  // MetricsProvider:
+  void ProvideGeneralMetrics(ChromeUserMetricsExtension* uma_proto) override;
+
+  // Uses |profiles| as the source data for the next invocation of
+  // ProvideGeneralMetrics, rather than sourcing them from the
+  // StackSamplingProfiler.
+  void SetSourceProfilesForTesting(
+      const std::vector<base::StackSamplingProfiler::Profile>& profiles);
+
+ private:
+  std::vector<base::StackSamplingProfiler::Profile> source_profiles_for_test_;
+
+  DISALLOW_COPY_AND_ASSIGN(CallStackProfileMetricsProvider);
+};
+
+}  // namespace metrics
+
+#endif  // COMPONENTS_METRICS_CALL_STACK_PROFILE_METRICS_PROVIDER_H_
diff --git a/components/metrics/call_stack_profile_metrics_provider_unittest.cc b/components/metrics/call_stack_profile_metrics_provider_unittest.cc
new file mode 100644
index 0000000..20e53d60
--- /dev/null
+++ b/components/metrics/call_stack_profile_metrics_provider_unittest.cc
@@ -0,0 +1,418 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/metrics/call_stack_profile_metrics_provider.h"
+
+#include "base/profiler/stack_sampling_profiler.h"
+#include "base/strings/string_number_conversions.h"
+#include "components/metrics/proto/chrome_user_metrics_extension.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::StackSamplingProfiler;
+using Frame = StackSamplingProfiler::Frame;
+using Module = StackSamplingProfiler::Module;
+using Profile = StackSamplingProfiler::Profile;
+using Sample = StackSamplingProfiler::Sample;
+
+namespace metrics {
+
+// Checks that all properties from multiple profiles are filled as expected.
+TEST(CallStackProfileMetricsProviderTest, MultipleProfiles) {
+  const uintptr_t module1_base_address = 0x1000;
+  const uintptr_t module2_base_address = 0x2000;
+  const uintptr_t module3_base_address = 0x3000;
+
+  const Module profile_modules[][2] = {
+    {
+      Module(
+          reinterpret_cast<const void*>(module1_base_address),
+          "ABCD",
+#if defined(OS_WIN)
+          base::FilePath(L"c:\\some\\path\\to\\chrome.exe")
+#else
+          base::FilePath("/some/path/to/chrome")
+#endif
+      ),
+      Module(
+          reinterpret_cast<const void*>(module2_base_address),
+          "EFGH",
+#if defined(OS_WIN)
+          base::FilePath(L"c:\\some\\path\\to\\third_party.dll")
+#else
+          base::FilePath("/some/path/to/third_party.so")
+#endif
+      ),
+    },
+    {
+      Module(
+          reinterpret_cast<const void*>(module3_base_address),
+          "MNOP",
+#if defined(OS_WIN)
+          base::FilePath(L"c:\\some\\path\\to\\third_party2.dll")
+#else
+          base::FilePath("/some/path/to/third_party2.so")
+#endif
+      ),
+      Module( // Repeated from the first profile.
+          reinterpret_cast<const void*>(module1_base_address),
+          "ABCD",
+#if defined(OS_WIN)
+          base::FilePath(L"c:\\some\\path\\to\\chrome.exe")
+#else
+          base::FilePath("/some/path/to/chrome")
+#endif
+      )
+    }
+  };
+
+  // Values for Windows generated with:
+  // perl -MDigest::MD5=md5 -MEncode=encode
+  //     -e 'for(@ARGV){printf "%x\n", unpack "Q>", md5 encode "UTF-16LE", $_}'
+  //     chrome.exe third_party.dll third_party2.dll
+  //
+  // Values for Linux generated with:
+  // perl -MDigest::MD5=md5
+  //     -e 'for(@ARGV){printf "%x\n", unpack "Q>", md5 $_}'
+  //     chrome third_party.so third_party2.so
+  const uint64 profile_expected_name_md5_prefixes[][2] = {
+    {
+#if defined(OS_WIN)
+      0x46c3e4166659ac02ULL,
+      0x7e2b8bfddeae1abaULL
+#else
+      0x554838a8451ac36cUL,
+      0x843661148659c9f8UL
+#endif
+    },
+    {
+#if defined(OS_WIN)
+      0x87b66f4573a4d5caULL,
+      0x46c3e4166659ac02ULL
+#else
+      0xb4647e539fa6ec9eUL,
+      0x554838a8451ac36cUL
+#endif
+    }
+  };
+
+  // Represents two stack samples for each of two profiles, where each stack
+  // contains three frames. Each frame contains an instruction pointer and a
+  // module index corresponding to the module for the profile in
+  // profile_modules.
+  //
+  // So, the first stack sample below has its top frame in module 0 at an offset
+  // of 0x10 from the module's base address, the next-to-top frame in module 1
+  // at an offset of 0x20 from the module's base address, and the bottom frame
+  // in module 0 at an offset of 0x30 from the module's base address
+  const Frame profile_sample_frames[][2][3] = {
+    {
+      {
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x10), 0),
+        Frame(reinterpret_cast<const void*>(module2_base_address + 0x20), 1),
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x30), 0)
+      },
+      {
+        Frame(reinterpret_cast<const void*>(module2_base_address + 0x10), 1),
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x20), 0),
+        Frame(reinterpret_cast<const void*>(module2_base_address + 0x30), 1)
+      }
+    },
+    {
+      {
+        Frame(reinterpret_cast<const void*>(module3_base_address + 0x10), 0),
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x20), 1),
+        Frame(reinterpret_cast<const void*>(module3_base_address + 0x30), 0)
+      },
+      {
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x10), 1),
+        Frame(reinterpret_cast<const void*>(module3_base_address + 0x20), 0),
+        Frame(reinterpret_cast<const void*>(module1_base_address + 0x30), 1)
+      }
+    }
+  };
+
+  base::TimeDelta profile_durations[2] = {
+    base::TimeDelta::FromMilliseconds(100),
+    base::TimeDelta::FromMilliseconds(200)
+  };
+
+  base::TimeDelta profile_sampling_periods[2] = {
+    base::TimeDelta::FromMilliseconds(10),
+    base::TimeDelta::FromMilliseconds(20)
+  };
+
+  std::vector<Profile> profiles;
+  for (size_t i = 0; i < arraysize(profile_sample_frames); ++i) {
+    Profile profile;
+    profile.modules.insert(
+        profile.modules.end(), &profile_modules[i][0],
+        &profile_modules[i][0] + arraysize(profile_modules[i]));
+
+    for (size_t j = 0; j < arraysize(profile_sample_frames[i]); ++j) {
+      profile.samples.push_back(Sample());
+      Sample& sample = profile.samples.back();
+      sample.insert(sample.end(), &profile_sample_frames[i][j][0],
+                    &profile_sample_frames[i][j][0] +
+                    arraysize(profile_sample_frames[i][j]));
+    }
+
+    profile.profile_duration = profile_durations[i];
+    profile.sampling_period = profile_sampling_periods[i];
+    profile.preserve_sample_ordering = false;
+
+    profiles.push_back(profile);
+  }
+
+  CallStackProfileMetricsProvider provider;
+  provider.SetSourceProfilesForTesting(profiles);
+  ChromeUserMetricsExtension uma_proto;
+  provider.ProvideGeneralMetrics(&uma_proto);
+
+  ASSERT_EQ(static_cast<int>(arraysize(profile_sample_frames)),
+            uma_proto.sampled_profile().size());
+  for (size_t i = 0; i < arraysize(profile_sample_frames); ++i) {
+    SCOPED_TRACE("profile " + base::IntToString(i));
+    const SampledProfile& sampled_profile = uma_proto.sampled_profile().Get(i);
+    ASSERT_TRUE(sampled_profile.has_call_stack_profile());
+    const CallStackProfile& call_stack_profile =
+        sampled_profile.call_stack_profile();
+
+    ASSERT_EQ(static_cast<int>(arraysize(profile_sample_frames[i])),
+              call_stack_profile.sample().size());
+    for (size_t j = 0; j < arraysize(profile_sample_frames[i]); ++j) {
+      SCOPED_TRACE("sample " + base::IntToString(j));
+      const CallStackProfile::Sample& proto_sample =
+          call_stack_profile.sample().Get(j);
+      ASSERT_EQ(static_cast<int>(arraysize(profile_sample_frames[i][j])),
+                proto_sample.entry().size());
+      ASSERT_TRUE(proto_sample.has_count());
+      EXPECT_EQ(1u, proto_sample.count());
+      for (size_t k = 0; k < arraysize(profile_sample_frames[i][j]); ++k) {
+        SCOPED_TRACE("frame " + base::IntToString(k));
+        const CallStackProfile::Entry& entry = proto_sample.entry().Get(k);
+        ASSERT_TRUE(entry.has_address());
+        const char* instruction_pointer = reinterpret_cast<const char*>(
+            profile_sample_frames[i][j][k].instruction_pointer);
+        const char* module_base_address = reinterpret_cast<const char*>(
+            profile_modules[i][profile_sample_frames[i][j][k].module_index]
+            .base_address);
+        EXPECT_EQ(static_cast<uint64>(instruction_pointer -
+                                      module_base_address), entry.address());
+        ASSERT_TRUE(entry.has_module_id_index());
+        EXPECT_EQ(profile_sample_frames[i][j][k].module_index,
+                  entry.module_id_index());
+      }
+    }
+
+    ASSERT_EQ(static_cast<int>(arraysize(profile_modules[i])),
+              call_stack_profile.module_id().size());
+    for (size_t j = 0; j < arraysize(profile_modules[i]); ++j) {
+      SCOPED_TRACE("module " + base::IntToString(j));
+      const CallStackProfile::ModuleIdentifier& module_identifier =
+          call_stack_profile.module_id().Get(j);
+      ASSERT_TRUE(module_identifier.has_build_id());
+      EXPECT_EQ(profile_modules[i][j].id, module_identifier.build_id());
+      ASSERT_TRUE(module_identifier.has_name_md5_prefix());
+      EXPECT_EQ(profile_expected_name_md5_prefixes[i][j],
+                module_identifier.name_md5_prefix());
+    }
+
+    ASSERT_TRUE(call_stack_profile.has_profile_duration_ms());
+    EXPECT_EQ(profile_durations[i].InMilliseconds(),
+              call_stack_profile.profile_duration_ms());
+    ASSERT_TRUE(call_stack_profile.has_sampling_period_ms());
+    EXPECT_EQ(profile_sampling_periods[i].InMilliseconds(),
+              call_stack_profile.sampling_period_ms());
+  }
+}
+
+// Checks that all duplicate samples are collapsed with
+// preserve_sample_ordering = false.
+TEST(CallStackProfileMetricsProviderTest, RepeatedStacksUnordered) {
+  const uintptr_t module_base_address = 0x1000;
+
+  const Module modules[] = {
+    Module(
+        reinterpret_cast<const void*>(module_base_address),
+        "ABCD",
+#if defined(OS_WIN)
+        base::FilePath(L"c:\\some\\path\\to\\chrome.exe")
+#else
+        base::FilePath("/some/path/to/chrome")
+#endif
+    )
+  };
+
+  // Duplicate samples in slots 0, 2, and 3.
+  const Frame sample_frames[][1] = {
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x20), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0) }
+  };
+
+  Profile profile;
+  profile.modules.insert(profile.modules.end(), &modules[0],
+      &modules[0] + arraysize(modules));
+
+  for (size_t i = 0; i < arraysize(sample_frames); ++i) {
+    profile.samples.push_back(Sample());
+    Sample& sample = profile.samples.back();
+    sample.insert(sample.end(), &sample_frames[i][0],
+                  &sample_frames[i][0] + arraysize(sample_frames[i]));
+  }
+
+  profile.profile_duration = base::TimeDelta::FromMilliseconds(100);
+  profile.sampling_period = base::TimeDelta::FromMilliseconds(10);
+  profile.preserve_sample_ordering = false;
+
+  CallStackProfileMetricsProvider provider;
+  provider.SetSourceProfilesForTesting(std::vector<Profile>(1, profile));
+  ChromeUserMetricsExtension uma_proto;
+  provider.ProvideGeneralMetrics(&uma_proto);
+
+  ASSERT_EQ(1, uma_proto.sampled_profile().size());
+  const SampledProfile& sampled_profile = uma_proto.sampled_profile().Get(0);
+  ASSERT_TRUE(sampled_profile.has_call_stack_profile());
+  const CallStackProfile& call_stack_profile =
+      sampled_profile.call_stack_profile();
+
+  ASSERT_EQ(2, call_stack_profile.sample().size());
+  for (int i = 0; i < 2; ++i) {
+    SCOPED_TRACE("sample " + base::IntToString(i));
+    const CallStackProfile::Sample& proto_sample =
+        call_stack_profile.sample().Get(i);
+    ASSERT_EQ(static_cast<int>(arraysize(sample_frames[i])),
+              proto_sample.entry().size());
+    ASSERT_TRUE(proto_sample.has_count());
+    EXPECT_EQ(i == 0 ? 3u : 1u, proto_sample.count());
+    for (size_t j = 0; j < arraysize(sample_frames[i]); ++j) {
+      SCOPED_TRACE("frame " + base::IntToString(j));
+      const CallStackProfile::Entry& entry = proto_sample.entry().Get(j);
+      ASSERT_TRUE(entry.has_address());
+      const char* instruction_pointer = reinterpret_cast<const char*>(
+          sample_frames[i][j].instruction_pointer);
+      const char* module_base_address = reinterpret_cast<const char*>(
+          modules[sample_frames[i][j].module_index].base_address);
+      EXPECT_EQ(static_cast<uint64>(instruction_pointer - module_base_address),
+                entry.address());
+      ASSERT_TRUE(entry.has_module_id_index());
+      EXPECT_EQ(sample_frames[i][j].module_index, entry.module_id_index());
+    }
+  }
+}
+
+// Checks that only contiguous duplicate samples are collapsed with
+// preserve_sample_ordering = true.
+TEST(CallStackProfileMetricsProviderTest, RepeatedStacksOrdered) {
+  const uintptr_t module_base_address = 0x1000;
+
+  const Module modules[] = {
+    Module(
+        reinterpret_cast<const void*>(module_base_address),
+        "ABCD",
+#if defined(OS_WIN)
+        base::FilePath(L"c:\\some\\path\\to\\chrome.exe")
+#else
+        base::FilePath("/some/path/to/chrome")
+#endif
+    )
+  };
+
+  // Duplicate samples in slots 0, 2, and 3.
+  const Frame sample_frames[][1] = {
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x20), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0), },
+    { Frame(reinterpret_cast<const void*>(module_base_address + 0x10), 0) }
+  };
+
+  Profile profile;
+  profile.modules.insert(profile.modules.end(), &modules[0],
+      &modules[0] + arraysize(modules));
+
+  for (size_t i = 0; i < arraysize(sample_frames); ++i) {
+    profile.samples.push_back(Sample());
+    Sample& sample = profile.samples.back();
+    sample.insert(sample.end(), &sample_frames[i][0],
+                  &sample_frames[i][0] + arraysize(sample_frames[i]));
+  }
+
+  profile.profile_duration = base::TimeDelta::FromMilliseconds(100);
+  profile.sampling_period = base::TimeDelta::FromMilliseconds(10);
+  profile.preserve_sample_ordering = true;
+
+  CallStackProfileMetricsProvider provider;
+  provider.SetSourceProfilesForTesting(std::vector<Profile>(1, profile));
+  ChromeUserMetricsExtension uma_proto;
+  provider.ProvideGeneralMetrics(&uma_proto);
+
+  ASSERT_EQ(1, uma_proto.sampled_profile().size());
+  const SampledProfile& sampled_profile = uma_proto.sampled_profile().Get(0);
+  ASSERT_TRUE(sampled_profile.has_call_stack_profile());
+  const CallStackProfile& call_stack_profile =
+      sampled_profile.call_stack_profile();
+
+  ASSERT_EQ(3, call_stack_profile.sample().size());
+  for (int i = 0; i < 3; ++i) {
+    SCOPED_TRACE("sample " + base::IntToString(i));
+    const CallStackProfile::Sample& proto_sample =
+        call_stack_profile.sample().Get(i);
+    ASSERT_EQ(static_cast<int>(arraysize(sample_frames[i])),
+              proto_sample.entry().size());
+    ASSERT_TRUE(proto_sample.has_count());
+    EXPECT_EQ(i == 2 ? 2u : 1u, proto_sample.count());
+    for (size_t j = 0; j < arraysize(sample_frames[i]); ++j) {
+      SCOPED_TRACE("frame " + base::IntToString(j));
+      const CallStackProfile::Entry& entry = proto_sample.entry().Get(j);
+      ASSERT_TRUE(entry.has_address());
+      const char* instruction_pointer = reinterpret_cast<const char*>(
+          sample_frames[i][j].instruction_pointer);
+      const char* module_base_address = reinterpret_cast<const char*>(
+          modules[sample_frames[i][j].module_index].base_address);
+      EXPECT_EQ(static_cast<uint64>(instruction_pointer - module_base_address),
+                entry.address());
+      ASSERT_TRUE(entry.has_module_id_index());
+      EXPECT_EQ(sample_frames[i][j].module_index, entry.module_id_index());
+    }
+  }
+}
+
+
+// Checks that unknown modules produce an empty Entry.
+TEST(CallStackProfileMetricsProviderTest, UnknownModule) {
+  // -1 indicates an unknown module.
+  const Frame frame(reinterpret_cast<const void*>(0x1000), -1);
+
+  Profile profile;
+
+  profile.samples.push_back(Sample(1, frame));
+
+  profile.profile_duration = base::TimeDelta::FromMilliseconds(100);
+  profile.sampling_period = base::TimeDelta::FromMilliseconds(10);
+  profile.preserve_sample_ordering = false;
+
+  CallStackProfileMetricsProvider provider;
+  provider.SetSourceProfilesForTesting(std::vector<Profile>(1, profile));
+  ChromeUserMetricsExtension uma_proto;
+  provider.ProvideGeneralMetrics(&uma_proto);
+
+  ASSERT_EQ(1, uma_proto.sampled_profile().size());
+  const SampledProfile& sampled_profile = uma_proto.sampled_profile().Get(0);
+  ASSERT_TRUE(sampled_profile.has_call_stack_profile());
+  const CallStackProfile& call_stack_profile =
+      sampled_profile.call_stack_profile();
+
+  ASSERT_EQ(1, call_stack_profile.sample().size());
+  const CallStackProfile::Sample& proto_sample =
+      call_stack_profile.sample().Get(0);
+  ASSERT_EQ(1, proto_sample.entry().size());
+  ASSERT_TRUE(proto_sample.has_count());
+  EXPECT_EQ(1u, proto_sample.count());
+  const CallStackProfile::Entry& entry = proto_sample.entry().Get(0);
+  EXPECT_FALSE(entry.has_address());
+  EXPECT_FALSE(entry.has_module_id_index());
+}
+
+}  // namespace metrics
diff --git a/components/metrics/proto/BUILD.gn b/components/metrics/proto/BUILD.gn
index c46f499..56ccd13 100644
--- a/components/metrics/proto/BUILD.gn
+++ b/components/metrics/proto/BUILD.gn
@@ -7,6 +7,7 @@
 # GYP version: components/
 proto_library("proto") {
   sources = [
+    "call_stack_profile.proto",
     "cast_logs.proto",
     "chrome_user_metrics_extension.proto",
     "histogram_event.proto",
diff --git a/components/metrics/proto/call_stack_profile.proto b/components/metrics/proto/call_stack_profile.proto
new file mode 100644
index 0000000..93fe6e1
--- /dev/null
+++ b/components/metrics/proto/call_stack_profile.proto
@@ -0,0 +1,61 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Call stack sample data for a given profiling session.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package metrics;
+
+// Next tag: 5
+message CallStackProfile {
+  // Describes an entry in the callstack.
+  message Entry {
+    // Instruction pointer subtracted by module base.
+    optional uint64 address = 1;
+
+    // Index to the module identifier in |module_ids| of CallStackProfile.
+    optional int32 module_id_index = 2;
+  }
+
+  // A sample consisting of one or more callstacks with the same stack frames
+  // and instruction pointers.
+  message Sample {
+    // The callstack. Sample.entries[0] represents the call on the top of the
+    // stack.
+    repeated Entry entry = 1;
+
+    // Number of times this stack signature occurs.
+    optional int64 count = 2;
+  }
+
+  // Uniquely identifies a module.
+  message ModuleIdentifier {
+    // A hash that uniquely identifies a particular program version with high
+    // probability. This is parsed from headers of the loaded module.
+    // For binaries generated by GNU tools:
+    //   Contents of the .note.gnu.build-id field.
+    // On Windows:
+    //   GUID + AGE in the debug image headers of a module.
+    optional bytes build_id = 1;
+
+    // MD5Sum Prefix of the module name. This is the same hashing scheme as used
+    // to hash UMA histogram names.
+    optional fixed64 name_md5_prefix = 2;
+  }
+
+  // The callstack and counts.
+  repeated Sample sample = 1;
+
+  // List of module ids found in this sample.
+  repeated ModuleIdentifier module_id = 2;
+
+  // Duration of this profile.
+  optional int32 profile_duration_ms = 3;
+
+  // Time between samples.
+  optional int32 sampling_period_ms = 4;
+}
diff --git a/components/metrics/proto/sampled_profile.proto b/components/metrics/proto/sampled_profile.proto
index 1b8d66f..5725309 100644
--- a/components/metrics/proto/sampled_profile.proto
+++ b/components/metrics/proto/sampled_profile.proto
@@ -8,12 +8,13 @@
 
 package metrics;
 
+import "call_stack_profile.proto";
 import "perf_data.proto";
 
 // Protocol buffer for collected sample-based profiling data.
 // Contains the parameters and data from a single profile collection event.
 
-// Next tag: 9
+// Next tag: 10
 message SampledProfile {
   // Indicates the event that triggered this collection.
   enum TriggerEvent {
@@ -59,6 +60,9 @@
   // collected. Only set when |trigger_event| is RESTORE_SESSION.
   optional int64 ms_after_restore = 8;
 
-  // The actual perf data that was collected.
+  // Sampled profile data collected from Linux perf tool.
   optional PerfDataProto perf_data = 4;
+
+  // Sampled profile data collected by periodic sampling of call stacks.
+  optional CallStackProfile call_stack_profile = 9;
 }