Add sampling memory profiling. accepted/tizen/unified/20230616.172407
authorMikhail Kurinnoi <m.kurinnoi@samsung.com>
Fri, 19 May 2023 15:45:35 +0000 (18:45 +0300)
committerGleb Balykov/Advanced System SW Lab /SRR/Staff Engineer/Samsung Electronics <g.balykov@samsung.com>
Tue, 6 Jun 2023 17:43:42 +0000 (20:43 +0300)
README.md
src/config/configurationmanager.cpp
src/config/profilerconfig.cpp
src/config/profilerconfig.h
src/info/objectinfo.cpp
src/info/objectinfo.h
src/profiler.cpp
src/trace/memorytrace.cpp
src/trace/memorytrace.h
src/tracelog/tracelog.cpp

index ad29944ed8180777d1782e0496f75ea13cd3dba8..5ae4e7139f8e258b7a8398e4f58614ada6fabf1d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -147,6 +147,19 @@ dumped (only timestamp of execution and configuration parameters is stored).
         Must be greater or equal to `PROF_MEMORY_MIN_SIZE_LIMIT` or will be disabled.
         Default value is `0` (limit is disabled).
 
+      - `PROF_MEMORY_SAMPLING_ENABLE` - sampling memory profiling mode by objects allocation size.
+        Default value is `false` (disabled).
+
+      - `PROF_MEMORY_SAMPLING_INTERVAL` - sampling memory profiling interval (size) by objects allocation size.
+        Default value is `512`.
+
+      - `PROF_MEMORY_SAMPLING_RANDOMIZATION`
+        Poisson Point Process is used for randomization of points on line segment (https://en.wikipedia.org/wiki/Poisson_point_process).
+        Using it we'll place points randomly on line with all allocations, so that more points get to larger allocations
+        and less points get to smaller allocations. This randomization is needed for cases when there are lots of small
+        allocations, which might be stepped over with constant sampling interval `PROF_MEMORY_SAMPLING_INTERVAL`.
+        Default value is `true`.
+
   - `PROF_STACK_DEPTH_LIMIT` - track changes of execution stack for first
     `PROF_STACK_DEPTH_LIMIT` frames only. Default value is `0` (limit is disabled).
     _Current limitation: `PROF_EXECUTION_TRACE` or `PROF_MEMORY_TRACE`
index 78ef6942f57bb801ddb0e2eddaa85b988d6d152d..4c31e0c5628d91b0d58d5f135af82d31fcec0553 100644 (file)
@@ -195,6 +195,10 @@ void ConfigurationManager::FetchConfig(ProfilerConfig &config) const
     FetchValue("PROF_GC_TRACE_GEN", new_config.GcGenerationBoundsTraceEnabled);
     FetchValue("PROF_GC_TRACE_ALT", new_config.GcAllocTableTraceEnabled);
 
+    FetchValue("PROF_MEMORY_SAMPLING_ENABLE", new_config.MemorySamplingEnabled);
+    FetchValue("PROF_MEMORY_SAMPLING_RANDOMIZATION", new_config.MemorySamplingIntervalRandomization);
+    FetchValue("PROF_MEMORY_SAMPLING_INTERVAL", new_config.MemorySamplingInterval);
+
     // Apply changes to the current configuration.
     config = new_config;
 }
index 0b3c69cca8f4162742c416b2e04522f987888bab..4613d0246d1199fbb6f7d3818e09dd5d7435652b 100644 (file)
@@ -39,6 +39,9 @@ ProfilerConfig::ProfilerConfig()
     , MemoryMaxSizeLimit(0)
     , GcGenerationBoundsTraceEnabled(false)
     , GcAllocTableTraceEnabled(false)
+    , MemorySamplingEnabled(false)
+    , MemorySamplingIntervalRandomization(true)
+    , MemorySamplingInterval(512)
 {}
 
 void ProfilerConfig::Validate()
@@ -169,6 +172,11 @@ std::vector<std::string> ProfilerConfig::Verify()
             warnings.push_back(
                 "GC allocations table tracing is memory tracing option");
         }
+
+        if (MemorySamplingEnabled)
+        {
+            warnings.push_back("memory sampling is memory tracing option");
+        }
     }
 
     if (MemoryMaxSizeLimit && MemoryMinSizeLimit > MemoryMaxSizeLimit)
index 83c679ed91ca512626302edfaf2e056f302c5a8b..d021611cfcfe256470aedcf2a0fd2edabe820a51 100644 (file)
@@ -77,6 +77,9 @@ struct ProfilerConfig
     unsigned long    MemoryMaxSizeLimit;
     bool             GcGenerationBoundsTraceEnabled;
     bool             GcAllocTableTraceEnabled;
+    bool             MemorySamplingEnabled;
+    bool             MemorySamplingIntervalRandomization;
+    unsigned long    MemorySamplingInterval;
 
     //
     // Validation and verification.
index 46d04f715b8e63f91ff842c7627b54646835c073..909f81ddc97d868946b73a96071e278e34e8a722 100644 (file)
 
 #include <exception>
 #include <stdexcept>
+#include <random>
+#include <cmath>
 
 #include "profiler.h"
 #include "classstorage.h"
 #include "classinfo.h"
 #include "objectinfo.h"
 
+template<class T>
+class UniformRandomGenerator
+{
+  std::random_device rd;
+  std::default_random_engine generator;
+  std::uniform_real_distribution<T> distribution;
+
+public:
+
+  UniformRandomGenerator(T start, T end)
+  : generator(rd()), distribution(start, end)
+  {}
+
+  double get()
+  {
+    return distribution(generator);
+  }
+};
+
+// distance to next sample in bytes (value <= 0 means that it's time to take a sample)
+static thread_local intptr_t g_distanceToNextSample = 0;
+// random number generator with values uniformly distributed on the [0.0,1.0) interval
+static thread_local UniformRandomGenerator<double> randgen(0.0, 1.0);
+
 __forceinline HRESULT ObjectInfo::InitializeType(
     const Profiler &profiler,
     ClassStorage &storage,
@@ -108,61 +134,60 @@ __forceinline HRESULT ObjectInfo::InitializeSize(
 
 HRESULT ObjectInfo::Initialize(
     const Profiler &profiler,
-    ClassStorage &storage) noexcept
+    ClassStorage &storage,
+    ClassID classId,
+    bool &skipByMemoryLimit,
+    bool *skipByMemorySampling) noexcept
 {
     HRESULT hrReturn = S_OK;
     HRESULT hr;
 
-    if (this->isInitialized)
-    {
-        return hrReturn;
-    }
-
     _ASSERTE(this->id != 0);
     const ProfilerInfo &info = profiler.GetProfilerInfo();
 
-    hr = this->InitializeType(profiler, storage, info);
+    hr = this->InitializeSize(profiler, info);
     if (FAILED(hr) && SUCCEEDED(hrReturn))
     {
         hrReturn = hr;
     }
 
-    hr = this->InitializeSize(profiler, info);
-    if (FAILED(hr) && SUCCEEDED(hrReturn))
+    if (this->size < profiler.GetConfig().MemoryMinSizeLimit ||
+        (profiler.GetConfig().MemoryMaxSizeLimit && this->size > profiler.GetConfig().MemoryMaxSizeLimit))
     {
-        hrReturn = hr;
+        skipByMemoryLimit = true;
+        return hrReturn;
     }
 
-    this->isInitialized = true;
-    return hrReturn;
-}
+    if (skipByMemorySampling && profiler.GetConfig().MemorySamplingEnabled)
+    {
+        // See dotnet/heaptrack PR #121 for more comments and code logic explanation.
 
-HRESULT ObjectInfo::Initialize(
-    const Profiler &profiler,
-    ClassStorage &storage,
-    ClassID classId) noexcept
-{
-    HRESULT hrReturn = S_OK;
-    HRESULT hr;
+        g_distanceToNextSample -= (intptr_t) this->size;
 
-    if (this->isInitialized)
-    {
-        return hrReturn;
-    }
+        if (g_distanceToNextSample > 0)
+        {
+            *skipByMemorySampling = true;
+            return hrReturn;
+        }
 
-    _ASSERTE(this->id != 0);
-    const ProfilerInfo &info = profiler.GetProfilerInfo();
+        intptr_t samples = - g_distanceToNextSample / profiler.GetConfig().MemorySamplingInterval;
+        g_distanceToNextSample %= profiler.GetConfig().MemorySamplingInterval;
 
-    hr = this->InitializeSize(profiler, info);
-    if (FAILED(hr) && SUCCEEDED(hrReturn))
-    {
-        hrReturn = hr;
-    }
+        do
+        {
+            intptr_t nextSamplingInterval = profiler.GetConfig().MemorySamplingInterval;
+            if (profiler.GetConfig().MemorySamplingIntervalRandomization)
+            {
+                double probability = randgen.get();
+                nextSamplingInterval = uintptr_t(- std::log(probability) * profiler.GetConfig().MemorySamplingInterval);
+            }
+
+            g_distanceToNextSample += nextSamplingInterval;
+            ++samples;
+        }
+        while (g_distanceToNextSample <= 0);
 
-    if (this->size < profiler.GetConfig().MemoryMinSizeLimit ||
-        (profiler.GetConfig().MemoryMaxSizeLimit && this->size > profiler.GetConfig().MemoryMaxSizeLimit))
-    {
-        return hrReturn;
+        this->size = samples * profiler.GetConfig().MemorySamplingInterval;
     }
 
     hr = this->InitializeTypeFromClassId(profiler, storage, classId);
@@ -171,6 +196,5 @@ HRESULT ObjectInfo::Initialize(
         hrReturn = hr;
     }
 
-    this->isInitialized = true;
     return hrReturn;
 }
index 97b862546059e619fa2bc9f1f948382843c44435..a1ddedb331db7054abef58a6058aa64e1a4c8424 100644 (file)
@@ -32,7 +32,6 @@ struct ObjectInfo
     ObjectID    id;
     SIZE_T      size;
     ClassInfo*  type;
-    bool        isInitialized;
 
 private:
     HRESULT InitializeType(
@@ -49,15 +48,13 @@ private:
         const Profiler &profiler,
         const ProfilerInfo &info) noexcept;
 
-    HRESULT Initialize(
-        const Profiler &profiler,
-        ClassStorage &storage) noexcept;
-
 public:
     HRESULT Initialize(
         const Profiler &profiler,
         ClassStorage &storage,
-        ClassID classId) noexcept;
+        ClassID classId,
+        bool &skipByMemoryLimit,
+        bool *skipByMemorySampling) noexcept;
 };
 
 #endif // _OBJECT_INFO_H_
index aa1bdb4d467d2457faa0e4eddf229c2e13536977..7f11adb657a47f40458f99ce841e9dc386b60bf9 100644 (file)
@@ -877,7 +877,12 @@ HRESULT STDMETHODCALLTYPE Profiler::MovedReferences(
     ULONG cObjectIDRangeLength[])
 {
     LOG().Trace() << "MovedReferences()";
-    return S_OK;
+
+    HRESULT hr;
+    hr = m_memoryTrace.MovedReferences(cMovedObjectIDRanges, oldObjectIDRangeStart,
+                                       newObjectIDRangeStart, cObjectIDRangeLength);
+
+    return hr;
 }
 
 HRESULT STDMETHODCALLTYPE Profiler::ObjectAllocated(
index 5217f3c8fd93c3036ca7b770acff37f53858148f..bfc04e9e72c1f1e0caaacdc4853f29e0dbb4bf6f 100644 (file)
@@ -137,16 +137,36 @@ HRESULT MemoryTrace::InitAllocInfoByTypes(AllocTable &allocInfoByTypes) noexcept
     HRESULT hr = S_OK;
     try
     {
+        std::lock_guard<std::mutex> lock(m_sampledObjectsMutex);
+        std::unordered_map<ObjectID, SIZE_T> survivedSampledObjects;
+
         auto storage_lock = m_profiler.GetCommonTrace().GetClassStorage();
         for (const auto &objectWithClass : m_survivedObjects)
         {
             ObjectID objectId = objectWithClass.first;
+
+            SIZE_T sampledSize = 0;
+            if (m_profiler.GetConfig().MemorySamplingEnabled)
+            {
+                auto find = m_sampledObjects.find(objectId);
+                if (find == m_sampledObjects.end())
+                {
+                    continue;
+                }
+                else
+                {
+                    sampledSize = find->second;
+                    survivedSampledObjects.emplace(std::move(*find));
+                }
+            }
+
             ClassID  classId  = objectWithClass.second;
 
             ObjectInfo objInfo = {};
             objInfo.id = objectId;
+            bool skipByMemoryLimit = false;
             // NOTE: it is OK to use classId == 0 here.
-            hr = objInfo.Initialize(m_profiler, *storage_lock, classId);
+            hr = objInfo.Initialize(m_profiler, *storage_lock, classId, skipByMemoryLimit, nullptr);
             if (FAILED(hr))
             {
                 throw std::runtime_error(
@@ -155,12 +175,17 @@ HRESULT MemoryTrace::InitAllocInfoByTypes(AllocTable &allocInfoByTypes) noexcept
                 );
             }
 
-            if (objInfo.size < m_profiler.GetConfig().MemoryMinSizeLimit ||
-                (m_profiler.GetConfig().MemoryMaxSizeLimit && objInfo.size > m_profiler.GetConfig().MemoryMaxSizeLimit))
+            if (skipByMemoryLimit)
             {
+                assert(!m_profiler.GetConfig().MemorySamplingEnabled && !sampledSize);
                 continue;
             }
 
+            if (sampledSize)
+            {
+                objInfo.size = sampledSize;
+            }
+
             _ASSERTE(objInfo.type != nullptr);
             if (objInfo.type->needPrintLoadFinished)
             {
@@ -178,6 +203,11 @@ HRESULT MemoryTrace::InitAllocInfoByTypes(AllocTable &allocInfoByTypes) noexcept
             allocInfo.allocCount++;
             allocInfo.memSize += objInfo.size;
         }
+
+        if (m_profiler.GetConfig().MemorySamplingEnabled)
+        {
+            m_sampledObjects = std::move(survivedSampledObjects);
+        }
     }
     catch (const std::exception &e)
     {
@@ -202,9 +232,11 @@ HRESULT MemoryTrace::ObjectAllocated(
     {
         ObjectInfo objInfo = {};
         objInfo.id = objectId;
+        bool skipByMemoryLimit = false;
+        bool skipByMemorySampling = false;
         {
             auto storage_lock = m_profiler.GetCommonTrace().GetClassStorage();
-            hr = objInfo.Initialize(m_profiler, *storage_lock, classId);
+            hr = objInfo.Initialize(m_profiler, *storage_lock, classId, skipByMemoryLimit, &skipByMemorySampling);
         }
         if (FAILED(hr))
         {
@@ -214,12 +246,17 @@ HRESULT MemoryTrace::ObjectAllocated(
             );
         }
 
-        if (objInfo.size < m_profiler.GetConfig().MemoryMinSizeLimit ||
-            (m_profiler.GetConfig().MemoryMaxSizeLimit && objInfo.size > m_profiler.GetConfig().MemoryMaxSizeLimit))
+        if (skipByMemoryLimit || skipByMemorySampling)
         {
             return hr;
         }
 
+        if (m_profiler.GetConfig().MemorySamplingEnabled)
+        {
+            std::lock_guard<std::mutex> lock(m_sampledObjectsMutex);
+            m_sampledObjects.emplace(objectId, objInfo.size);
+        }
+
         _ASSERTE(objInfo.type != nullptr);
         if (objInfo.type->needPrintLoadFinished)
         {
@@ -265,6 +302,47 @@ HRESULT MemoryTrace::ObjectAllocated(
     return hr;
 }
 
+HRESULT MemoryTrace::MovedReferences(
+    ULONG cMovedObjectIDRanges,
+    ObjectID oldObjectIDRangeStart[],
+    ObjectID newObjectIDRangeStart[],
+    ULONG cObjectIDRangeLength[]) noexcept
+{
+    if (!m_profiler.GetConfig().MemorySamplingEnabled)
+        return S_OK;
+
+    std::lock_guard<std::mutex> lock(m_sampledObjectsMutex);
+
+    std::map<ObjectID, SIZE_T> oldObjects(m_sampledObjects.begin(), m_sampledObjects.end());
+    m_sampledObjects.clear();
+
+    for (ULONG i = 0; i < cMovedObjectIDRanges; i++)
+    {
+        auto start = oldObjects.lower_bound(oldObjectIDRangeStart[i]);
+
+        for (auto it = start; it != oldObjects.end(); )
+        {
+            // https://github.com/dotnet/runtime/blob/aef327f3b418bf5dd4f25083aff160c9cdf4b159/src/coreclr/inc/corprof.idl#L1666-L1669
+            if (oldObjectIDRangeStart[i] <= it->first && it->first < oldObjectIDRangeStart[i] + cObjectIDRangeLength[i])
+            {
+                m_sampledObjects.emplace(it->first - oldObjectIDRangeStart[i] + newObjectIDRangeStart[i], it->second);
+                it = oldObjects.erase(it);
+                continue;
+            }
+
+            break;
+        }
+    }
+
+    for (auto &object : oldObjects)
+    {
+        m_sampledObjects.emplace(std::move(object));
+    }
+    oldObjects.clear();
+
+    return S_OK;
+}
+
 HRESULT MemoryTrace::GarbageCollectionStarted(
     int cGenerations,
     BOOL generationCollected[],
index d9c9b0a020a4ff99fd92ce7c6068d2f996c4d30a..da45a62ed05c5093b8bfab927c55fe205203c8cb 100644 (file)
@@ -19,6 +19,7 @@
 
 #include <vector>
 #include <utility>
+#include <unordered_map>
 
 #include <cor.h>
 #include <corhdr.h>
@@ -64,6 +65,12 @@ public:
         ObjectID objectId,
         ClassID classId) noexcept;
 
+    HRESULT MovedReferences(
+        ULONG cMovedObjectIDRanges,
+        ObjectID oldObjectIDRangeStart[],
+        ObjectID newObjectIDRangeStart[],
+        ULONG cObjectIDRangeLength[]) noexcept;
+
     HRESULT GarbageCollectionStarted(
         int cGenerations,
         BOOL generationCollected[],
@@ -81,6 +88,9 @@ private:
     bool m_objectTrackingSuspended;
     bool m_objectTrackingFailure;
     std::vector<std::pair<ObjectID, ClassID>> m_survivedObjects;
+
+    std::mutex m_sampledObjectsMutex;
+    std::unordered_map<ObjectID, SIZE_T> m_sampledObjects;
 };
 
 #endif // _MEMORY_TRACE_H_
index a468bc460978ad23a4b9d1272461e0ffd74f945c..c3879f04d985acfbad55de6a42674b0065b7a104 100644 (file)
@@ -160,6 +160,9 @@ public:
         m_tracefmt.log("prf cfg", g_tls_ss).str("MemoryMinSizeLimit").config(config.MemoryMinSizeLimit).end();
         m_tracefmt.log("prf cfg", g_tls_ss).str("MemoryMaxSizeLimit").config(config.MemoryMaxSizeLimit).end();
         m_tracefmt.log("prf cfg", g_tls_ss).str("GcAllocTableTraceEnabled").config(config.GcAllocTableTraceEnabled).end();
+        m_tracefmt.log("prf cfg", g_tls_ss).str("MemorySamplingEnabled").config(config.MemorySamplingEnabled).end();
+        m_tracefmt.log("prf cfg", g_tls_ss).str("MemorySamplingIntervalRandomization").config(config.MemorySamplingIntervalRandomization).end();
+        m_tracefmt.log("prf cfg", g_tls_ss).str("MemorySamplingInterval").config(config.MemorySamplingInterval).end();
         AddTLSDataToQueue();
     }