[soft navs] Mojom Refactor (Update,Commit,FCP) and FCP in UKM [chromium/src : main]

0 views
Skip to first unread message

Johannes Henkel (Gerrit)

unread,
Jun 29, 2026, 11:02:23 PM (9 days ago) Jun 29
to Michal Mocny, Annie Sullivan, Chromium LUCI CQ, Chromium Metrics Reviews, chromium...@chromium.org, alexmo...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, blink-revi...@chromium.org, blink-...@chromium.org, bmcquad...@chromium.org, core-timi...@chromium.org, core-web-vita...@chromium.org, creis...@chromium.org, csharris...@chromium.org, ipc-securi...@chromium.org, loading-rev...@chromium.org, navigation...@chromium.org, speed-metrics...@chromium.org, speed-metr...@chromium.org
Attention needed from Annie Sullivan and Michal Mocny

Johannes Henkel added 4 comments

File chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc
Line 203, Patchset 1: duration: record.duration,
Johannes Henkel . resolved

'duration' is the thingy from the paint timing mixin, and it's the delta between the startTime of the navigation and the presentation time - so it's exactly what we want for comparing with the UKM logged FCP.

Line 334, Patchset 1: std::vector<double> soft_nav_start_times;
for (const auto& [source_id, start_time] : source_id_to_start_time) {
soft_nav_start_times.push_back(start_time);
}
Johannes Henkel . resolved

I moved this part up here (without changing anything) to make it easier to read (since this section is about getting the start times).

Line 339, Patchset 1: // Soft navigation FCP.
auto source_id_to_fcp = GetSoftNavigationMetrics(
ukm_recorder(),
SoftNavigation::kPaintTiming_FirstContentfulPaintMsName);
EXPECT_EQ(source_id_to_fcp.size(), expected_soft_nav_count);
std::vector<double> soft_nav_fcp;
for (const auto& [source_id, fcp] : source_id_to_fcp) {
soft_nav_fcp.push_back(fcp);
}
Johannes Henkel . resolved

This is new, it extracts the soft fcp that is recorded for UKM.

Line 392, Patchset 1: EXPECT_NEAR(*timing.FindDoubleByDottedPath("softNavigation.duration"),
soft_nav_fcp[i], 6);
Johannes Henkel . resolved

Here we compare the UKM recorded FCP with the paint timing mixin's duration.

Open in Gerrit

Related details

Attention is currently required from:
  • Annie Sullivan
  • Michal Mocny
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: I65058e5a18921660945b4753b3b154bf7bd6647a
Gerrit-Change-Number: 8021904
Gerrit-PatchSet: 2
Gerrit-Owner: Johannes Henkel <joha...@chromium.org>
Gerrit-Reviewer: Annie Sullivan <sull...@chromium.org>
Gerrit-Reviewer: Johannes Henkel <joha...@chromium.org>
Gerrit-Reviewer: Michal Mocny <mmo...@chromium.org>
Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
Gerrit-Attention: Annie Sullivan <sull...@chromium.org>
Gerrit-Attention: Michal Mocny <mmo...@chromium.org>
Gerrit-Comment-Date: Tue, 30 Jun 2026 03:02:09 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
satisfied_requirement
unsatisfied_requirement
open
diffy

Johannes Henkel (Gerrit)

unread,
Jul 8, 2026, 8:19:34 PM (8 hours ago) Jul 8
to Michal Mocny, Annie Sullivan, Chromium LUCI CQ, Chromium Metrics Reviews, chromium...@chromium.org, alexmo...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, blink-revi...@chromium.org, blink-...@chromium.org, bmcquad...@chromium.org, core-timi...@chromium.org, core-web-vita...@chromium.org, creis...@chromium.org, csharris...@chromium.org, ipc-securi...@chromium.org, loading-rev...@chromium.org, navigation...@chromium.org, speed-metrics...@chromium.org, speed-metr...@chromium.org
Attention needed from Annie Sullivan and Michal Mocny

Johannes Henkel added 24 comments

File chrome/browser/page_load_metrics/integration_tests/soft_navigation_metrics_browsertest.cc
Line 203, Patchset 19 (Latest): duration: record.duration,
Johannes Henkel . resolved

extracting the duration from the performance timeline API, so we can compare it below (with the soft FCP)

Line 340, Patchset 19 (Parent): std::vector<double> soft_nav_start_times;

for (const auto& [source_id, start_time] : source_id_to_start_time) {
soft_nav_start_times.push_back(start_time);
}
Johannes Henkel . resolved

moved this up a bit, for readability

Line 339, Patchset 19 (Latest): // Soft navigation FCP.

auto source_id_to_fcp = GetSoftNavigationMetrics(
ukm_recorder(),
SoftNavigation::kPaintTiming_FirstContentfulPaintMsName);
EXPECT_EQ(source_id_to_fcp.size(), expected_soft_nav_count);
std::vector<double> soft_nav_fcp;
for (const auto& [source_id, fcp] : source_id_to_fcp) {
soft_nav_fcp.push_back(fcp);
}
Johannes Henkel . resolved

We get the UKM recorded soft FCP and check that each softnav has one.

Line 392, Patchset 19 (Latest): EXPECT_NEAR(*timing.FindDoubleByDottedPath("softNavigation.duration"),
soft_nav_fcp[i], 6);
Johannes Henkel . resolved

This checks that the Soft FCP in UKM and the duration on the performance timeline match.

File chrome/browser/page_load_metrics/observers/core/ukm_page_load_metrics_observer_unittest.cc
Line 1875, Patchset 19 (Parent): tester()->SimulateSoftNavigation(&navigation_handle);
Johannes Henkel . resolved

This interface was unnecessary / weird - all we're doing here is committing a same document navigation; so now I'm using SimulateDidFinishNavigation instead, and I've removed SimulateSoftNavigation.

Line 1870, Patchset 19 (Latest): // Now simulate one SoftNavigationUpdate.
std::vector<page_load_metrics::mojom::SoftNavigationUpdatePtr>
soft_navigation_updates;
soft_navigation_updates.emplace_back(
page_load_metrics::mojom::SoftNavigationUpdate::New(
/*soft_navigation_offset=*/1,
page_load_metrics::mojom::SoftNavigationUpdatePayload::NewCommit(
page_load_metrics::mojom::SoftNavigationCommit::New(
/*start_time=*/base::Milliseconds(12),
/*soft_navigation_slicing_time=*/base::TimeTicks() +
base::Milliseconds(12),
/*navigation_type=*/
blink::mojom::NavigationTypeForNavigationApi::kPush,
/*same_document_metrics_token=*/
base::UnguessableToken::Create()))));
tester()->SimulateSoftNavigationUpdates(soft_navigation_updates);
Johannes Henkel . resolved

This interface is modernized in that we send soft navigation updates. A single update (no FCP) is needed to be counted as a soft nav.

File components/page_load_metrics/browser/fake_page_load_metrics_observer_delegate.h
Line 131, Patchset 19 (Latest): mojom::SoftNavigationMetricsPtr soft_navigation_metrics_;
Johannes Henkel . resolved

Added this field just so what's returned by GetSoftNavigationMetrics() is owned by something.

File components/page_load_metrics/browser/fake_page_load_metrics_observer_delegate.cc
Line 218, Patchset 19 (Parent): return *mojom::SoftNavigationMetrics::New();
Johannes Henkel . resolved

I figured it's better to stick this into a field, it seems a bit sketch to dereference the temporary...

File components/page_load_metrics/browser/metrics_web_contents_observer.h
Line 187, Patchset 19 (Latest): std::vector<mojom::SoftNavigationUpdatePtr> soft_navigation_updates,
std::vector<mojom::LargestContentfulPaintTimingPtr>
soft_largest_contentful_paint,
Johannes Henkel . resolved

So in this change, on the browser side, all the UpdateTiming and equivalent calls have a vector of SoftNavigationUpdate and a vector of soft LCP. In a later cl, I've put the soft LCP also into the updates, which has the advantage of being more efficient and retaining the order of insertion.

So if you don't see the point of sending updates, I think looking at that follow-up cl shows it:
https://chromium-review.git.corp.google.com/c/chromium/src/+/8029244/8/components/page_load_metrics/browser/metrics_web_contents_observer.h (and other files in that cl).

File components/page_load_metrics/browser/observers/page_load_metrics_observer_tester.h
Line 122, Patchset 19 (Latest): void SimulateSoftNavigationUpdates(
const std::vector<mojom::SoftNavigationUpdatePtr>& updates);
Johannes Henkel . resolved

Changed this to updates, it's more honest with the refactoring; but this also means the test code changes a bit (see ukm_page_load_metrics_observer_unittest.cc).

Line 120, Patchset 19 (Parent): void SimulateSoftNavigation(content::NavigationHandle* navigation_handle);
Johannes Henkel . resolved

This method delegates to SimulateDidFinishNavigation anyway, so I got rid of it. The point is to commit a same-document navigation, which is how the ukm_page_load_metric_observer.cc gets the ukm source id for the soft nav.

File components/page_load_metrics/browser/observers/page_load_metrics_observer_tester.cc
Line 281, Patchset 19 (Parent): std::vector<mojom::EventTimingPtr> event_timings_clone;
for (const auto& entry : event_timings) {
event_timings_clone.push_back(entry.Clone());
}
Johannes Henkel . resolved

This can just use mojo::Clone so I did that while I'm in here.

File components/page_load_metrics/browser/observers/soft_navigation_page_load_metrics_observer.cc
Line 239, Patchset 19 (Latest):void SoftNavigationPageLoadMetricsObserver::RecordSoftFcp(
ukm::builders::SoftNavigation& builder,
const SoftNavigationMetrics& metrics,
const SoftNavigationCommit& commit) {
if (metrics.first_contentful_paint.has_value() &&
FromForegroundOptionalEventInForeground(
metrics.first_contentful_paint.value())) {
base::TimeDelta soft_fcp =
(metrics.first_contentful_paint.value() - commit.start_time);
builder.SetPaintTiming_FirstContentfulPaintMs(soft_fcp.InMilliseconds());
}
}
Johannes Henkel . resolved

Here we now record soft FCP to UKM, on a best-effort basis.

The logic to delay soft navs processing until presentation time (FCP) is received is in a subsequent cl, and there it's mostly in soft_navigation_tracker.cc.

File components/page_load_metrics/browser/page_load_metrics_update_dispatcher.cc
Line 610, Patchset 19 (Latest): CHECK(!soft_navigation_tracker_.HasNextSoftNavigation());
if (!soft_navigation_tracker_.ValidateAndProcessUpdates(
std::move(soft_navigation_updates))) {
return;
}
while (true) {
soft_navigation_tracker_.Process(
&event_timings, &soft_navigation_interaction_to_next_paint_);
soft_navigation_tracker_.Process(
&layout_shifts, &soft_navigation_layout_shift_normalization_);
size_t num_soft_lcps_processed = soft_navigation_tracker_.Process(
&soft_lcps, &soft_navigation_largest_contentful_paint_);
if (num_soft_lcps_processed) {
client_->OnSoftNavigationLargestContentfulPaint(num_soft_lcps_processed);
}
if (!soft_navigation_tracker_.HasNextSoftNavigation()) {
break;
}
client_->OnSoftNavigation(); // Notify observers, via PageLoadTracker.
soft_navigation_tracker_.AdvanceToNextSoftNavigation();
soft_navigation_interaction_to_next_paint_.ClearEventTimings();
soft_navigation_layout_shift_normalization_.ClearAllLayoutShifts();
soft_navigation_largest_contentful_paint_.Clear();
}
Johannes Henkel . resolved

Note how the processing loop hasn't changed significantly here, while we process stuff in order in which the soft navs are received, all data that came across the UpdateTiming is processed within the body of this method and soft navs are not kept between calls - even if the FCP for some soft nav is missing; see the DCHECK at the beginning of this section.

Only in a subsequent change is this loop substantially modified so that the soft navigation tracker and the calculators keep state between calls.

File components/page_load_metrics/browser/soft_navigation_tracker.cc
Line 47, Patchset 19 (Latest):bool SoftNavigationTracker::ValidateAndProcessUpdates(
Johannes Henkel . resolved

The scheme for this validation is: Make a copy of the state, add the new stuff in the copied queue, if we see a problem bail (leaving behind the object unmodified), if it's good swap the modified state in.

Line 64, Patchset 19 (Latest): switch (update->payload->which()) {
Johannes Henkel . resolved

This is one of the things I really like about the union, it ensures all cases are covered.

File components/page_load_metrics/common/page_load_metrics.mojom
Line 708, Patchset 19 (Latest):// Soft navigation metrics (see SoftNavigationMetrics) are sent as
// SoftNavigationUpdate from the renderer to the browser as soon as they are
// available; therefore, we tag the pieces with the `soft_navigation_offset`,
// and the payload is the union of the individual components.
struct SoftNavigationUpdate {
// The one-based offset of the interaction in time-based order; 1 for the
// first interaction, 2 for the second, etc.
uint64 soft_navigation_offset;

SoftNavigationUpdatePayload payload;
};
Johannes Henkel . resolved

This is the main innovation in this change, when things get sent from renderer to browser, it's sent as SoftNavigationUpdate, and a union (the payload) identifies what type of update.

Though it's lame to just have two types of updates, the later cl allows the soft LCP as well, reducing the number of arguments to UpdateTiming.

https://chromium-review.git.corp.google.com/c/chromium/src/+/8029244/8/components/page_load_metrics/common/page_load_metrics.mojom

File components/page_load_metrics/renderer/fake_page_timing_sender.cc
Line 37, Patchset 19 (Latest): for (auto& update : soft_navigation_updates) {
if (update->payload && update->payload->is_commit()) {
auto metrics = mojom::SoftNavigationMetrics::New();
metrics->soft_navigation_offset = update->soft_navigation_offset;
metrics->commit = std::move(update->payload->get_commit());
soft_navigation_metrics.push_back(std::move(metrics));
}
}
Johannes Henkel . resolved

OK, maybe I should plumb this into UpdateTiming as well, instead of converting back to SoftNavigationMetrics. Will look into it.

File components/page_load_metrics/renderer/metrics_render_frame_observer_unittest.cc
Line 258, Patchset 19 (Latest): validator_.ExpectSoftNavigationMetrics(*soft_navigation_metrics);
Johannes Henkel . unresolved

So yeah, maybe this should be expecting an update. It's a bit lame to have the SoftNavigationMetrics on the renderer side. Will look into this one.

File components/page_load_metrics/renderer/page_timing_metrics_sender.cc
Line 130, Patchset 19 (Latest):
CHECK(!commit.same_document_metrics_token.is_empty());
if (!soft_navigation_updates_.empty()) {
CHECK_EQ(soft_navigation_updates_.back()->soft_navigation_offset + 1,
commit.soft_navigation_offset);
if (soft_navigation_updates_.back()->payload->is_commit()) {
const auto& last_commit =
soft_navigation_updates_.back()->payload->get_commit();
CHECK_NE(commit.same_document_metrics_token,
last_commit->same_document_metrics_token);
}
}
Johannes Henkel . resolved

In a way, these checks are quite optional, since this is the renderer which we don't trust anyway.

What's important here is that the production code which enqueues the updates is *very* simple, it just constructs an update for the commit here, and one for the FCP below, and puts it into the soft_navigation_updates_ array.

In the later CL, same with soft LCP.
https://chromium-review.git.corp.google.com/c/chromium/src/+/8029244/8/components/page_load_metrics/renderer/page_timing_metrics_sender.cc

File third_party/blink/public/web/web_performance_metrics_for_reporting.h
Line 59, Patchset 19 (Latest):struct BLINK_EXPORT SoftNavigationCommitForReporting {
Johannes Henkel . resolved

Note while this metrics struct is now called "Commit", there is no struct for FCP. Reason being, FCP is just one value as payload, with a soft_navigation_offset to tag it, so we send these as two params directly.

File third_party/blink/renderer/core/frame/local_frame_client.h
Line 255, Patchset 19 (Latest): virtual void DidObserveSoftNavigation(
const SoftNavigationCommitForReporting& commit) {}
Johannes Henkel . resolved

I made all these structs const refs as suggested by Scott in an earlier attempt of mine.

File third_party/blink/renderer/core/timing/soft_navigation_heuristics.cc
Line 607, Patchset 19 (Latest): // TODO(crbug.com/490814752): Some tests simulate events with an impossibly
// small start_time value, which is less than the initial reference time,
// which makes the duration appear negative. We cannot report such values
// without failing expectations (in tests).
if (context->TimeOrigin() <= loader->GetTiming().ReferenceMonotonicTime()) {
return;
}
Johannes Henkel . resolved

I've started looking into this, but some tests are still failing if it's removed.

File tools/metrics/ukm/ukm.xml
Line 23542, Patchset 19 (Latest): <metric name="PaintTiming.FirstContentfulPaintMs">
<summary>
Measures the FirstContentfulPaint in milliseconds from the start time of
the soft navigation to the presentation time.
</summary>
</metric>
Johannes Henkel . resolved

Adding this lets us update the google3 stuff asap. :-)

Open in Gerrit

Related details

Attention is currently required from:
  • Annie Sullivan
  • Michal Mocny
Submit Requirements:
    • requirement satisfiedCode-Coverage
    • requirement is not satisfiedCode-Owners
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: chromium/src
    Gerrit-Branch: main
    Gerrit-Change-Id: I65058e5a18921660945b4753b3b154bf7bd6647a
    Gerrit-Change-Number: 8021904
    Gerrit-PatchSet: 19
    Gerrit-Owner: Johannes Henkel <joha...@chromium.org>
    Gerrit-Reviewer: Annie Sullivan <sull...@chromium.org>
    Gerrit-Reviewer: Johannes Henkel <joha...@chromium.org>
    Gerrit-Reviewer: Michal Mocny <mmo...@chromium.org>
    Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
    Gerrit-Attention: Annie Sullivan <sull...@chromium.org>
    Gerrit-Attention: Michal Mocny <mmo...@chromium.org>
    Gerrit-Comment-Date: Thu, 09 Jul 2026 00:19:20 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    satisfied_requirement
    unsatisfied_requirement
    open
    diffy

    Johannes Henkel (Gerrit)

    unread,
    Jul 8, 2026, 11:34:15 PM (5 hours ago) Jul 8
    to Michal Mocny, Annie Sullivan, Chromium LUCI CQ, Chromium Metrics Reviews, chromium...@chromium.org, alexmo...@chromium.org, asvitkine...@chromium.org, blink-re...@chromium.org, blink-revi...@chromium.org, blink-...@chromium.org, bmcquad...@chromium.org, core-timi...@chromium.org, core-web-vita...@chromium.org, creis...@chromium.org, csharris...@chromium.org, ipc-securi...@chromium.org, loading-rev...@chromium.org, navigation...@chromium.org, speed-metrics...@chromium.org, speed-metr...@chromium.org
    Attention needed from Annie Sullivan and Michal Mocny

    Johannes Henkel added 1 comment

    File components/page_load_metrics/renderer/metrics_render_frame_observer_unittest.cc
    Line 258, Patchset 19: validator_.ExpectSoftNavigationMetrics(*soft_navigation_metrics);
    Johannes Henkel . resolved

    So yeah, maybe this should be expecting an update. It's a bit lame to have the SoftNavigationMetrics on the renderer side. Will look into this one.

    Johannes Henkel

    ok this is fixed (patchset 20/21)

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Annie Sullivan
    • Michal Mocny
    Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement is not satisfiedCode-Owners
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: I65058e5a18921660945b4753b3b154bf7bd6647a
      Gerrit-Change-Number: 8021904
      Gerrit-PatchSet: 21
      Gerrit-Owner: Johannes Henkel <joha...@chromium.org>
      Gerrit-Reviewer: Annie Sullivan <sull...@chromium.org>
      Gerrit-Reviewer: Johannes Henkel <joha...@chromium.org>
      Gerrit-Reviewer: Michal Mocny <mmo...@chromium.org>
      Gerrit-CC: Chromium Metrics Reviews <chromium-met...@google.com>
      Gerrit-Attention: Annie Sullivan <sull...@chromium.org>
      Gerrit-Attention: Michal Mocny <mmo...@chromium.org>
      Gerrit-Comment-Date: Thu, 09 Jul 2026 03:34:01 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Johannes Henkel <joha...@chromium.org>
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy
      Reply all
      Reply to author
      Forward
      0 new messages