Repository: ydb-platform/nbs
Pull request #3567: Log first request execution time and offset from volume creation
Author: login: drbasic
Labels: name: large-tests
description: Launch large tests for PR
name: blockstore
description: Add this label to run only cloud/blockstore build and tests on PR
Created: 2025-05-24T11:58:02Z | Merged: 2025-05-29T04:56:32Z

Сделал что по логам видно, через сколько после создания актора, диск начал отвечать на запросы. Именно в секундах, а не искать в логах старт таблетки, потом смотреть ну вот теперь ретраи кончились, наверное диск заработал

```
2025-05-24T11:52:12.109400Z :BLOCKSTORE_VOLUME INFO: [72075186224037890] Disk: "vol0", Generation: 20. The first successful Read request was started at 3.503s finished at 3.504s. The very first request was started at 3.503s Failed requests: 0
2025-05-24T11:52:12.342278Z :BLOCKSTORE_VOLUME INFO: [72075186224037890] Disk: "vol0", Generation: 20. The first successful Write request was started at 3.707s finished at 3.737s. The very first request was started at 3.707s Failed requests: 0
```


Diff:
diff --git a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
index 1167258c79d..2352f16103c 100644
--- a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
+++ b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
@@ -7,6 +7,7 @@
 
 #include <util/datetime/cputimer.h>
 #include <util/string/builder.h>
+
 #include <span>
 
 namespace NCloud::NBlockStore::NStorage {
@@ -152,7 +153,8 @@ bool TRequestsTimeTracker::TEqual::operator()(
 
 ////////////////////////////////////////////////////////////////////////////////
 
-TRequestsTimeTracker::TRequestsTimeTracker()
+TRequestsTimeTracker::TRequestsTimeTracker(const ui64 constructionTime)
+    : ConstructionTime(constructionTime)
 {
     for (size_t sizeBucket = 0; sizeBucket <= TotalSizeBucket; ++sizeBucket) {
         for (size_t requestType = 0;
@@ -174,6 +176,12 @@ void TRequestsTimeTracker::OnRequestStarted(
     TBlockRange64 blockRange,
     ui64 startTime)
 {
+    const auto requestTypeIndex = static_cast<size_t>(requestType);
+    auto& firstRequest = FirstRequests[requestTypeIndex];
+    if (firstRequest.StartTime == 0) {
+        firstRequest.StartTime = startTime;
+    }
+
     InflightRequests.emplace(
         requestId,
         TRequestInflight{
@@ -182,7 +190,38 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+std::optional<TRequestsTimeTracker::TFirstSuccessStat>
+TRequestsTimeTracker::StatFirstSuccess(
+    const TRequestInflight& request,
+    bool success,
+    ui64 finishTime)
+{
+    const auto requestTypeIndex = static_cast<size_t>(request.RequestType);
+    auto& firstRequest = FirstRequests[requestTypeIndex];
+    if (firstRequest.FinishTime != 0) {
+        return std::nullopt;
+    }
+
+    if (!success) {
+        ++firstRequest.FailCount;
+        return std::nullopt;
+    }
+
+    firstRequest.FinishTime = finishTime;
+
+    return TFirstSuccessStat{
+        .RequestType = request.RequestType,
+        .FirstRequestStartTime =
+            CyclesToDurationSafe(firstRequest.StartTime - ConstructionTime),
+        .SuccessfulRequestStartTime =
+            CyclesToDurationSafe(request.StartTime - ConstructionTime),
+        .SuccessfulRequestFinishTime =
+            CyclesToDurationSafe(firstRequest.FinishTime - ConstructionTime),
+        .FailCount = firstRequest.FailCount};
+}
+
+std::optional<TRequestsTimeTracker::TFirstSuccessStat>
+TRequestsTimeTracker::OnRequestFinished(
     ui64 requestId,
     bool success,
     ui64 finishTime)
@@ -191,7 +230,7 @@ void TRequestsTimeTracker::OnRequestFinished(
     {
         auto it = InflightRequests.find(requestId);
         if (it == InflightRequests.end()) {
-            return;
+            return {};
         }
         request = it->second;
         InflightRequests.erase(requestId);
@@ -210,6 +249,8 @@ void TRequestsTimeTracker::OnRequestFinished(
     key.SizeBucket = TotalSizeBucket;
     Histograms[key].Increment(duration.MicroSeconds());
     Histograms[key].BlockCount += request.BlockRange.Size();
+
+    return StatFirstSuccess(request, success, finishTime);
 }
 
 NJson::TJsonValue TRequestsTimeTracker::BuildPercentilesJson() const
diff --git a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.h b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.h
index 1ce26fb8110..fd8647177cc 100644
--- a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.h
+++ b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker.h
@@ -27,6 +27,13 @@ class TRequestsTimeTracker
         Last = Describe,
     };
 
+    enum class ERequestStatus
+    {
+        Inflight,
+        Success,
+        Fail,
+    };
+
     struct TBucketInfo
     {
         TString Key;
@@ -34,14 +41,19 @@ class TRequestsTimeTracker
         TString Tooltip;
     };
 
-private:
-    enum class ERequestStatus
+    struct TFirstSuccessStat
     {
-        Inflight,
-        Success,
-        Fail,
+        ERequestType RequestType = ERequestType::Read;
+        TDuration FirstRequestStartTime;
+        TDuration SuccessfulRequestStartTime;
+        TDuration SuccessfulRequestFinishTime;
+        size_t FailCount = 0;
     };
 
+private:
+    constexpr static size_t RequestTypeCount =
+        static_cast<size_t>(ERequestType::Last) + 1;
+
     struct TTimeHistogram: public THistogram<TRequestUsTimeBuckets>
     {
         size_t BlockCount = 0;
@@ -78,13 +90,29 @@ class TRequestsTimeTracker
         ERequestType RequestType = ERequestType::Read;
     };
 
+    struct TFirstRequest
+    {
+        ui64 StartTime = 0;
+        ui64 FinishTime = 0;
+        size_t FailCount = 0;
+    };
+
+    const ui64 ConstructionTime;
+
+    std::array<TFirstRequest, RequestTypeCount> FirstRequests;
     THashMap<ui64, TRequestInflight> InflightRequests;
     THashMap<TKey, TTimeHistogram, THash, TEqual> Histograms;
 
     [[nodiscard]] NJson::TJsonValue BuildPercentilesJson() const;
 
+    [[nodiscard]] std::optional<TRequestsTimeTracker::TFirstSuccessStat>
+    StatFirstSuccess(
+        const TRequestInflight& request,
+        bool success,
+        ui64 finishTime);
+
 public:
-    explicit TRequestsTimeTracker();
+    explicit TRequestsTimeTracker(const ui64 constructionTime);
 
     static TVector<TBucketInfo> GetSizeBuckets(ui32 blockSize);
     static TVector<TBucketInfo> GetTimeBuckets();
@@ -96,7 +124,10 @@ class TRequestsTimeTracker
         TBlockRange64 blockRange,
         ui64 startTime);
 
-    void OnRequestFinished(ui64 requestId, bool success, ui64 finishTime);
+    // Marks that the request is completed and returns stat when the request
+    // succeeds for the first time.
+    [[nodiscard]] std::optional<TFirstSuccessStat>
+    OnRequestFinished(ui64 requestId, bool success, ui64 finishTime);
 
     [[nodiscard]] TString GetStatJson(ui64 nowCycles, ui32 blockSize) const;
 };
diff --git a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker_ut.cpp b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker_ut.cpp
index fce86a2a01a..dca69dde093 100644
--- a/cloud/blockstore/libs/storage/volume/model/requests_time_tracker_ut.cpp
+++ b/cloud/blockstore/libs/storage/volume/model/requests_time_tracker_ut.cpp
@@ -37,7 +37,7 @@ Y_UNIT_TEST_SUITE(TRequestsTimeTrackerTest)
 {
     Y_UNIT_TEST(ShouldCountInflight)
     {
-        TRequestsTimeTracker requestsTimeTracker;
+        TRequestsTimeTracker requestsTimeTracker(0);
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Read,
@@ -80,41 +80,61 @@ Y_UNIT_TEST_SUITE(TRequestsTimeTrackerTest)
 
     Y_UNIT_TEST(ShouldCountFinishedSuccess)
     {
-        TRequestsTimeTracker requestsTimeTracker;
+        TRequestsTimeTracker requestsTimeTracker(0);
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Write,
             1,
             TBlockRange64::MakeOneBlock(0),
-            0);
+            1000 * GetCyclesPerMillisecond());
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Write,
             2,
             TBlockRange64::MakeOneBlock(0),
-            1000 * GetCyclesPerMillisecond());
+            2000 * GetCyclesPerMillisecond());
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Write,
             3,
             TBlockRange64::MakeOneBlock(0),
-            2000 * GetCyclesPerMillisecond());
-
-        requestsTimeTracker.OnRequestFinished(
-            1,
-            true,
             3000 * GetCyclesPerMillisecond());
-        requestsTimeTracker.OnRequestFinished(
+
+        auto stat = requestsTimeTracker.OnRequestFinished(
             2,
             true,
-            3000 * GetCyclesPerMillisecond());
-        requestsTimeTracker.OnRequestFinished(
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_VALUES_EQUAL(
+            TRequestsTimeTracker::ERequestType::Write,
+            stat->RequestType);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(2000).SecondsFloat(),
+            stat->SuccessfulRequestStartTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(4000).SecondsFloat(),
+            stat->SuccessfulRequestFinishTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(1000).SecondsFloat(),
+            stat->FirstRequestStartTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_VALUES_EQUAL(0, stat->FailCount);
+
+        stat = requestsTimeTracker.OnRequestFinished(
+            1,
+            true,
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_EQUAL(std::nullopt, stat);
+
+        stat = requestsTimeTracker.OnRequestFinished(
             3,
             true,
-            3000 * GetCyclesPerMillisecond());
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_EQUAL(std::nullopt, stat);
 
         auto json = requestsTimeTracker.GetStatJson(
-            5000 * GetCyclesPerMillisecond(),
+            6000 * GetCyclesPerMillisecond(),
             4096);
         NJson::TJsonValue value;
         NJson::ReadJsonTree(json, &value, true);
@@ -153,41 +173,46 @@ Y_UNIT_TEST_SUITE(TRequestsTimeTrackerTest)
 
     Y_UNIT_TEST(ShouldCountFinishedFail)
     {
-        TRequestsTimeTracker requestsTimeTracker;
+        TRequestsTimeTracker requestsTimeTracker(0);
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Zero,
             1,
             TBlockRange64::WithLength(0, 512),
-            0);
+            1000 * GetCyclesPerMillisecond());
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Zero,
             2,
             TBlockRange64::WithLength(0, 600),
-            1000 * GetCyclesPerMillisecond());
+            2000 * GetCyclesPerMillisecond());
 
         requestsTimeTracker.OnRequestStarted(
             TRequestsTimeTracker::ERequestType::Zero,
             3,
             TBlockRange64::WithLength(0, 2000),
-            2000 * GetCyclesPerMillisecond());
+            3000 * GetCyclesPerMillisecond());
 
-        requestsTimeTracker.OnRequestFinished(
+        auto stat = requestsTimeTracker.OnRequestFinished(
             1,
             false,
-            3000 * GetCyclesPerMillisecond());
-        requestsTimeTracker.OnRequestFinished(
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_EQUAL(std::nullopt, stat);
+
+        stat = requestsTimeTracker.OnRequestFinished(
             2,
             false,
-            3000 * GetCyclesPerMillisecond());
-        requestsTimeTracker.OnRequestFinished(
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_EQUAL(std::nullopt, stat);
+
+        stat = requestsTimeTracker.OnRequestFinished(
             3,
             false,
-            3000 * GetCyclesPerMillisecond());
+            4000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_EQUAL(std::nullopt, stat);
 
         auto json = requestsTimeTracker.GetStatJson(
-            5000 * GetCyclesPerMillisecond(),
+            6000 * GetCyclesPerMillisecond(),
             4096);
         NJson::TJsonValue value;
         NJson::ReadJsonTree(json, &value, true);
@@ -220,6 +245,34 @@ Y_UNIT_TEST_SUITE(TRequestsTimeTrackerTest)
         const auto nonEmptyPercentilesCount =
             DumpValues(value["percentiles"].GetMap());
         UNIT_ASSERT_VALUES_EQUAL(0, nonEmptyPercentilesCount);
+
+        // Successful request
+        requestsTimeTracker.OnRequestStarted(
+            TRequestsTimeTracker::ERequestType::Zero,
+            4,
+            TBlockRange64::WithLength(0, 600),
+            7000 * GetCyclesPerMillisecond());
+
+        stat = requestsTimeTracker.OnRequestFinished(
+            4,
+            true,
+            10000 * GetCyclesPerMillisecond());
+        UNIT_ASSERT_VALUES_EQUAL(
+            TRequestsTimeTracker::ERequestType::Zero,
+            stat->RequestType);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(7000).SecondsFloat(),
+            stat->SuccessfulRequestStartTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(10000).SecondsFloat(),
+            stat->SuccessfulRequestFinishTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_DOUBLES_EQUAL(
+            TDuration::MilliSeconds(1000).SecondsFloat(),
+            stat->FirstRequestStartTime.SecondsFloat(),
+            1e-5);
+        UNIT_ASSERT_VALUES_EQUAL(3, stat->FailCount);
     }
 }
 
diff --git a/cloud/blockstore/libs/storage/volume/model/ya.make b/cloud/blockstore/libs/storage/volume/model/ya.make
index 8b7f9a5deae..2b3872d6a82 100644
--- a/cloud/blockstore/libs/storage/volume/model/ya.make
+++ b/cloud/blockstore/libs/storage/volume/model/ya.make
@@ -4,6 +4,7 @@ LIBRARY()
 
 GENERATE_ENUM_SERIALIZATION(checkpoint.h)
 GENERATE_ENUM_SERIALIZATION(follower_disk.h)
+GENERATE_ENUM_SERIALIZATION(requests_time_tracker.h)
 
 SRCS(
     checkpoint.cpp
diff --git a/cloud/blockstore/libs/storage/volume/volume_actor.h b/cloud/blockstore/libs/storage/volume/volume_actor.h
index 296cfe1711e..06e8db6a3ea 100644
--- a/cloud/blockstore/libs/storage/volume/volume_actor.h
+++ b/cloud/blockstore/libs/storage/volume/volume_actor.h
@@ -305,7 +305,8 @@ class TVolumeActor final
 
     TVolumeRequestMap VolumeRequests;
     TRequestsInFlight WriteAndZeroRequestsInFlight;
-    TRequestsTimeTracker RequestTimeTracker;
+
+    TRequestsTimeTracker RequestTimeTracker{GetCycleCount()};
     TTransactionTimeTracker TransactionTimeTracker;
 
     // inflight VolumeRequestId -> duplicate request queue
diff --git a/cloud/blockstore/libs/storage/volume/volume_actor_forward.cpp b/cloud/blockstore/libs/storage/volume/volume_actor_forward.cpp
index 1153c990bae..169c6e34fdd 100644
--- a/cloud/blockstore/libs/storage/volume/volume_actor_forward.cpp
+++ b/cloud/blockstore/libs/storage/volume/volume_actor_forward.cpp
@@ -12,6 +12,7 @@
 #include <cloud/blockstore/libs/storage/volume/model/merge.h>
 #include <cloud/blockstore/libs/storage/volume/model/stripe.h>
 
+#include <cloud/storage/core/libs/common/format.h>
 #include <cloud/storage/core/libs/common/media.h>
 #include <cloud/storage/core/libs/common/verify.h>
 #include <cloud/storage/core/libs/diagnostics/trace_serializer.h>
@@ -480,11 +481,28 @@ bool TVolumeActor::ReplyToOriginalRequest(
     }
 
     VolumeRequests.erase(it);
-    RequestTimeTracker.OnRequestFinished(
+    const auto firstSuccess = RequestTimeTracker.OnRequestFinished(
         volumeRequestId,
         success,
         GetCycleCount());
 
+    if (firstSuccess) {
+        LOG_INFO(
+            ctx,
+            TBlockStoreComponents::VOLUME,
+            "[%lu] Disk: %s, Generation: %u. The first successful %s "
+            "request was started at %s finished at %s. The very first request "
+            "was started at %s. Failed requests: %lu",
+            TabletID(),
+            State->GetDiskId().Quote().c_str(),
+            Executor()->Generation(),
+            ToString(firstSuccess->RequestType).c_str(),
+            FormatDuration(firstSuccess->SuccessfulRequestStartTime).c_str(),
+            FormatDuration(firstSuccess->SuccessfulRequestFinishTime).c_str(),
+            FormatDuration(firstSuccess->FirstRequestStartTime).c_str(),
+            firstSuccess->FailCount);
+    }
+
     return true;
 }
 


Reviews:
user: login: komarevtsev-d
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-26T06:36:36Z
user: login: SvartMetal
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T16:38:07Z
user: login: drbasic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T05:04:28Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T10:27:10Z
user: login: SvartMetal
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T16:43:25Z
user: login: SvartMetal
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-28T16:54:26Z
user: login: komarevtsev-d
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-29T04:52:17Z

Review comments:
diff_hunk: @@ -182,7 +190,39 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+TString TRequestsTimeTracker::CalcRequestFirstTime(
path: cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
user: login: SvartMetal
body: PrintFirstRequestTime
created_at: 2025-05-26T16:29:20Z
author_association: COLLABORATOR
diff_hunk: @@ -182,7 +190,39 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+TString TRequestsTimeTracker::CalcRequestFirstTime(
+    const TRequestInflight& request,
+    bool success,
+    ui64 finishTime)
+{
+    const auto requestTypeIndex = static_cast<size_t>(request.RequestType);
+    auto& firstRequest = FirstRequests[requestTypeIndex];
+    if (firstRequest.FinishTime != 0) {
+        return {};
+    }
+
+    if (!success) {
+        ++firstRequest.FailCount;
+        return {};
+    }
+
+    firstRequest.FinishTime = finishTime;
+
+    auto formatDuration = [&](ui64 finish)
+    {
+        return FormatDuration(CyclesToDurationSafe(finish - ConstructionTime));
+    };
+
+    return TStringBuilder()
+           << "The first successful " << ToString(request.RequestType)
+           << " request was started at " << formatDuration(request.StartTime)
+           << " finished at " << formatDuration(finishTime) << "."
+           << " The very first request was started at "
+           << formatDuration(firstRequest.StartTime)
+           << ", Failed requests: " << firstRequest.FailCount;
+}
+
+TString TRequestsTimeTracker::OnRequestFinished(
path: cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
user: login: SvartMetal
body: неочевидно

может залоггируем тут?

а сюда передадим logTag, с доп инфой https://github.com/ydb-platform/nbs/pull/3567/files#diff-0e8bbc41061139f49336020564a85e877e96b165f4b9b4ad72414f4754d47c5bR491
created_at: 2025-05-26T16:37:54Z
author_association: COLLABORATOR
diff_hunk: @@ -182,7 +190,39 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+TString TRequestsTimeTracker::CalcRequestFirstTime(
+    const TRequestInflight& request,
+    bool success,
+    ui64 finishTime)
+{
+    const auto requestTypeIndex = static_cast<size_t>(request.RequestType);
+    auto& firstRequest = FirstRequests[requestTypeIndex];
+    if (firstRequest.FinishTime != 0) {
+        return {};
+    }
+
+    if (!success) {
+        ++firstRequest.FailCount;
+        return {};
+    }
+
+    firstRequest.FinishTime = finishTime;
+
+    auto formatDuration = [&](ui64 finish)
+    {
+        return FormatDuration(CyclesToDurationSafe(finish - ConstructionTime));
+    };
+
+    return TStringBuilder()
+           << "The first successful " << ToString(request.RequestType)
+           << " request was started at " << formatDuration(request.StartTime)
+           << " finished at " << formatDuration(finishTime) << "."
+           << " The very first request was started at "
+           << formatDuration(firstRequest.StartTime)
+           << ", Failed requests: " << firstRequest.FailCount;
+}
+
+TString TRequestsTimeTracker::OnRequestFinished(
path: cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
user: login: drbasic
body: Да, я сначала начал делать как ты говоришь, но получилось не очень - нужно тащить в requests_time_tracker.cpp знание про диск, поколение и т.д. Если принести сразу готовую подстроку, то тоже не очень хорошо. 
created_at: 2025-05-27T05:04:28Z
author_association: COLLABORATOR
diff_hunk: @@ -182,7 +190,39 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+TString TRequestsTimeTracker::CalcRequestFirstTime(
+    const TRequestInflight& request,
+    bool success,
+    ui64 finishTime)
+{
+    const auto requestTypeIndex = static_cast<size_t>(request.RequestType);
+    auto& firstRequest = FirstRequests[requestTypeIndex];
+    if (firstRequest.FinishTime != 0) {
+        return {};
+    }
+
+    if (!success) {
+        ++firstRequest.FailCount;
+        return {};
+    }
+
+    firstRequest.FinishTime = finishTime;
+
+    auto formatDuration = [&](ui64 finish)
+    {
+        return FormatDuration(CyclesToDurationSafe(finish - ConstructionTime));
+    };
+
+    return TStringBuilder()
+           << "The first successful " << ToString(request.RequestType)
+           << " request was started at " << formatDuration(request.StartTime)
+           << " finished at " << formatDuration(finishTime) << "."
+           << " The very first request was started at "
+           << formatDuration(firstRequest.StartTime)
+           << ", Failed requests: " << firstRequest.FailCount;
+}
+
+TString TRequestsTimeTracker::OnRequestFinished(
path: cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
user: login: SvartMetal
body: С готовой подстрокой норм, так иногда делаем
created_at: 2025-05-28T10:27:10Z
author_association: COLLABORATOR
diff_hunk: @@ -182,7 +190,38 @@ void TRequestsTimeTracker::OnRequestStarted(
             .RequestType = requestType});
 }
 
-void TRequestsTimeTracker::OnRequestFinished(
+std::optional<TRequestsTimeTracker::TFirstSuccessStat>
+TRequestsTimeTracker::MakeRequestFirstTimeSucceedMessage(
path: cloud/blockstore/libs/storage/volume/model/requests_time_tracker.cpp
user: login: SvartMetal
body: StatFirstSuccess
created_at: 2025-05-28T16:43:24Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-24T14:01:42Z
body: <!-- status pr=3567, run=11802, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:red_circle: **linux-x86_64-relwithdebinfo**: some tests FAILED for commit 1997e80f1f16a849ec6f3b8f840b9fbaf1b98bae.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [5362](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/1/ya-test.html) | [5325](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/1/ya-test.html#PASS) | 0 | [37](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/1/ya-test.html#FAIL) | 0 | 0 |


:red_circle: **linux-x86_64-relwithdebinfo**: some tests FAILED for commit 1997e80f1f16a849ec6f3b8f840b9fbaf1b98bae.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [67](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/2/ya-test.html) | [30](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/2/ya-test.html#PASS) | 0 | [37](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/2/ya-test.html#FAIL) | 0 | 0 |


:red_circle: **linux-x86_64-relwithdebinfo**: some tests FAILED for commit 1997e80f1f16a849ec6f3b8f840b9fbaf1b98bae.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [67](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/3/ya-test.html) | [30](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/3/ya-test.html#PASS) | 0 | [37](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15226801039/1/nebius-x86-64/summary/3/ya-test.html#FAIL) | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-24T18:27:22Z
body: <!-- status pr=3567, run=11804, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 7c73bea632b81dafe6bdb5978edbb595c2537b92.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [5362](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15228845662/1/nebius-x86-64/summary/1/ya-test.html) | [5362](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15228845662/1/nebius-x86-64/summary/1/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: komarevtsev-d
created_at: 2025-05-26T06:22:18Z
body: А какая мотивация? Зачем это?
author_association: COLLABORATOR
user: login: drbasic
created_at: 2025-05-26T06:35:07Z
body: > А какая мотивация? Зачем это?

Я замучался разбираться по логам рестарта когда диск заработал. 
Это первый ПР. В нем сделано что по логам видно через сколько после создания актора диск начал отвечать на запросы. Именно в секундах, а не искать в логах старт таблетки, потом смотреть ну вот теперь ретраи кончились, наверное диск заработал. При том что в логах есть старты таблеток блю-грина - в которых тоже сложно разбираться. 
Во втором ПРе будет сделано чтобы было понятно какой это экземпляр таблетки.
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-27T07:17:19Z
body: <!-- status pr=3567, run=11835, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit f5eb73c761c0bf6d9a86dd0ab9550d8b63338bbc.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [5362](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15267066366/1/nebius-x86-64/summary/1/ya-test.html) | [5362](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15267066366/1/nebius-x86-64/summary/1/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-28T19:10:34Z
body: <!-- status pr=3567, run=11914, build_preset=linux-x86_64-relwithdebinfo, dry_run=False -->
> [!NOTE]
> This is an automated comment that will be appended during run.

:green_circle: **linux-x86_64-relwithdebinfo**: all tests PASSED for commit 58e9750d0884027a5460970602790b166dc05a04.
| TESTS | PASSED | ERRORS | FAILED | SKIPPED | MUTED<sup>[?](https://github.com/ydb-platform/nbs/tree/main/.github/config "All mute rules are defined here")</sup> |
| ---: | ---: | ---: | ---: | ---: | ---: |
| [5370](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15305883242/1/nebius-x86-64/summary/1/ya-test.html) | [5370](https://github-actions-s3.storage.eu-north1.nebius.cloud/ydb-platform/nbs/PR-check/15305883242/1/nebius-x86-64/summary/1/ya-test.html#PASS) | 0 | 0 | 0 | 0 |
author_association: CONTRIBUTOR