0

webview: update constructor performance metrics.

Add new metrics to properly distinguish the *three* distinct cases we
can encounter while creating a WebView instance.

The existing metrics relied on "has the global Chromium initialization
already run" to distinguish "cold" and "warm" cases, and these cases
were being interpreted as "whether this is the first instance of WebView
or not".

There are actually three cases: it's possible for the global Chromium
initialization to have already run *before* the first WebView was
constructed. The existing metrics categorized this as "cold", but the
first instance still takes longer to construct than subsequent
instances even in this case, making it hard to interpret the metrics
correctly.

Instead, define three separate histograms for the three cases. We
continue to log the old histograms as well for now to allow for
cross-version comparisons.

The descriptions of the existing metrics are also updated to clarify
their limitations and suggest consulting the new metrics instead.

Bug: b:373630656
Change-Id: Id927276f2bb6327bec3764e1fe6dd6abb2ffb1ef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6108730
Reviewed-by: Mark Pearson <mpearson@chromium.org>
Commit-Queue: Richard (Torne) Coles <torne@chromium.org>
Reviewed-by: Nate Fischer <ntfschr@chromium.org>
Auto-Submit: Richard (Torne) Coles <torne@chromium.org>
Reviewed-by: Oksana Zhuravlova <oksamyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1420668}
This commit is contained in:
Torne (Richard Coles)
2025-02-14 12:12:54 -08:00
committed by Chromium LUCI CQ
parent d803df3d7a
commit dcd4117a44
6 changed files with 222 additions and 59 deletions
android_webview/glue/java/src/com/android/webview/chromium
base/android
java
src
org
chromium
junit
src
org
trace_event_binding.cc
tools/metrics/histograms/metadata/android

@ -88,14 +88,15 @@ import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This class is the delegate to which WebViewProxy forwards all API calls.
*
* Most of the actual functionality is implemented by AwContents (or WebContents within
* it). This class also contains WebView-specific APIs that require the creation of other
* adapters (otherwise org.chromium.content would depend on the webview.chromium package)
* and a small set of no-op deprecated APIs.
* <p>Most of the actual functionality is implemented by AwContents (or WebContents within it). This
* class also contains WebView-specific APIs that require the creation of other adapters (otherwise
* org.chromium.content would depend on the webview.chromium package) and a small set of no-op
* deprecated APIs.
*/
@SuppressWarnings("deprecation")
@Lifetime.WebView
@ -136,6 +137,8 @@ class WebViewChromium
sRecordWholeDocumentEnabledByApi = true;
}
private static final AtomicBoolean sFirstWebViewInstanceCreated = new AtomicBoolean();
// Used to record the UMA histogram WebView.WebViewApiCall. Since these values are persisted to
// logs, they should never be renumbered or reused.
// LINT.IfChange(ApiCall)
@ -704,7 +707,8 @@ class WebViewChromium
public void init(
final Map<String, Object> javaScriptInterfaces, final boolean privateBrowsing) {
long startTime = SystemClock.uptimeMillis();
boolean isFirstWebViewInit = !mFactory.isChromiumInitialized();
boolean wasChromiumAlreadyInitialized = mFactory.isChromiumInitialized();
boolean isFirstWebViewInstance = !sFirstWebViewInstanceCreated.getAndSet(true);
try (ScopedSysTraceEvent e1 = ScopedSysTraceEvent.scoped("WebViewChromium.init")) {
if (privateBrowsing) {
mFactory.startYourEngines(true);
@ -750,6 +754,10 @@ class WebViewChromium
mFactory.startYourEngines(true);
}
// At this point it is guaranteed that global Chromium init has completed on the UI
// thread, as all paths have called startYourEngines. However, this function itself is
// *not* necessarily running on the UI thread due to the pre-JBMR2 case above.
final boolean isAccessFromFileUrlsGrantedByDefault =
mAppTargetSdkVersion < Build.VERSION_CODES.JELLY_BEAN;
final boolean areLegacyQuirksEnabled =
@ -794,6 +802,9 @@ class WebViewChromium
mSharedWebViewChromium.init(mContentsClientAdapter);
// In the normal case where we are currently on the UI thread, this will run initForReal
// synchronously. For pre-JBMR2 apps we might not be on the UI thread, in which case it
// will be posted and we do not wait for it.
mFactory.addTask(
new Runnable() {
@Override
@ -811,31 +822,42 @@ class WebViewChromium
});
}
// If initialization hasn't been deferred, record a startup time histogram entry
// and trace event(s).
if (mFactory.isChromiumInitialized()) {
if (isFirstWebViewInit) {
long elapsedTime = SystemClock.uptimeMillis() - startTime;
if (isFirstWebViewInstance) {
if (wasChromiumAlreadyInitialized) {
// This is the first WebView created, but global Chromium initialization happened
// before the constructor was called.
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.Stage2.ProviderInit.Cold",
SystemClock.uptimeMillis() - startTime);
TraceEvent.webViewStartupTotalFactoryInit(
mFactory.getInitInfo().mTotalFactoryInitStartTime,
mFactory.getInitInfo().mTotalFactoryInitDuration);
TraceEvent.webViewStartupStage1(
mFactory.getInitInfo().mStartTime, mFactory.getInitInfo().mDuration);
TraceEvent.webViewStartupStage2(
startTime, SystemClock.uptimeMillis() - startTime, true);
"Android.WebView.Startup.CreationTime.FirstInstanceAfterGlobalStartup",
elapsedTime);
TraceEvent.webViewStartupFirstInstance(startTime, elapsedTime, false);
} else {
// This is the first WebView created, and we blocked running global Chromium
// initialization during the constructor.
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.Stage2.ProviderInit.Warm",
SystemClock.uptimeMillis() - startTime);
TraceEvent.webViewStartupStage2(
startTime, SystemClock.uptimeMillis() - startTime, false);
"Android.WebView.Startup.CreationTime.FirstInstanceWithGlobalStartup",
elapsedTime);
TraceEvent.webViewStartupFirstInstance(startTime, elapsedTime, true);
}
} else {
// This is not the first WebView created; global Chromium initialization must have
// happened beforehand.
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.NotFirstInstance", elapsedTime);
TraceEvent.webViewStartupNotFirstInstance(startTime, elapsedTime);
}
// Record "legacy" metrics. These have suboptimal definitions because they don't allow for
// the case where global Chromium initialization happened before the first WebView instance
// was constructed, and just use "cold/warm" to refer to whether global Chromium
// initialization had to be run during the constructor or not, giving the "cold" case a
// bimodal distribution.
if (!wasChromiumAlreadyInitialized) {
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.Stage2.ProviderInit.Cold", elapsedTime);
} else {
RecordHistogram.recordTimesHistogram(
"Android.WebView.Startup.CreationTime.Stage2.ProviderInit.Warm", elapsedTime);
}
}

@ -433,6 +433,13 @@ public class WebViewChromiumAwInit {
totalTimeTaken,
/* callSite= */ callSite,
/* fromUIThread= */ triggeredFromUIThread);
// Also create the trace events for the earlier WebViewChromiumFactoryProvider init, which
// happens before tracing is ready.
TraceEvent.webViewStartupTotalFactoryInit(
mFactory.getInitInfo().mTotalFactoryInitStartTime,
mFactory.getInitInfo().mTotalFactoryInitDuration);
TraceEvent.webViewStartupStage1(
mFactory.getInitInfo().mStartTime, mFactory.getInitInfo().mDuration);
}
/**

@ -442,15 +442,27 @@ public class TraceEvent implements AutoCloseable {
}
/**
* Records 'WebView.Startup.CreationTime.Stage2.ProviderInit.Warm' and
* 'WebView.Startup.CreationTime.Stage2.ProviderInit.Cold' events depending on the value of
* `isColdStartup` with the 'android_webview.timeline' category starting at `startTimeMs` with
* the duration of `durationMs`.
* Records 'WebView.Startup.CreationTime.FirstInstanceWithGlobalStartup' or
* 'WebView.Startup.CreationTime.FirstInstanceWithoutGlobalStartup' events depending on the
* value of `includedGlobalStartup` with the 'android_webview.timeline' category starting at
* `startTimeMs` with the duration of `durationMs`.
*/
public static void webViewStartupStage2(
long startTimeMs, long durationMs, boolean isColdStartup) {
public static void webViewStartupFirstInstance(
long startTimeMs, long durationMs, boolean includedGlobalStartup) {
if (sEnabled) {
TraceEventJni.get().webViewStartupStage2(startTimeMs, durationMs, isColdStartup);
TraceEventJni.get()
.webViewStartupFirstInstance(startTimeMs, durationMs, includedGlobalStartup);
}
}
/**
* Records a 'WebView.Startup.CreationTime.NotFirstInstance' event with the
* 'android_webview.timeline' category starting at `startTimeMs` with the duration of
* `durationMs`.
*/
public static void webViewStartupNotFirstInstance(long startTimeMs, long durationMs) {
if (sEnabled) {
TraceEventJni.get().webViewStartupNotFirstInstance(startTimeMs, durationMs);
}
}
@ -672,7 +684,10 @@ public class TraceEvent implements AutoCloseable {
void webViewStartupStage1(long startTimeMs, long durationMs);
void webViewStartupStage2(long startTimeMs, long durationMs, boolean isColdStartup);
void webViewStartupFirstInstance(
long startTimeMs, long durationMs, boolean includedGlobalStartup);
void webViewStartupNotFirstInstance(long startTimeMs, long durationMs);
void webViewStartupStartChromiumLocked(
long startTimeMs, long durationMs, int callSite, boolean fromUIThread);

@ -122,13 +122,25 @@ public class TraceEventTest {
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testWebViewStartupStage2() {
public void testWebViewStartupFirstInstance() {
TraceEvent.setEnabled(true);
long startTime = 10;
long duration = 50;
boolean isCold = true;
TraceEvent.webViewStartupStage2(startTime, duration, isCold);
verify(mNativeMock).webViewStartupStage2(startTime, duration, isCold);
boolean includedGlobalStartup = true;
TraceEvent.webViewStartupFirstInstance(startTime, duration, includedGlobalStartup);
verify(mNativeMock).webViewStartupFirstInstance(startTime, duration, includedGlobalStartup);
TraceEvent.setEnabled(false);
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testWebViewStartupNotFirstInstance() {
TraceEvent.setEnabled(true);
long startTime = 10;
long duration = 50;
TraceEvent.webViewStartupNotFirstInstance(startTime, duration);
verify(mNativeMock).webViewStartupNotFirstInstance(startTime, duration);
TraceEvent.setEnabled(false);
}
}

@ -306,21 +306,24 @@ static void JNI_TraceEvent_WebViewStartupStage1(JNIEnv* env,
#endif // BUILDFLAG(ENABLE_BASE_TRACING)
}
static void JNI_TraceEvent_WebViewStartupStage2(JNIEnv* env,
jlong start_time_ms,
jlong duration_ms,
jboolean is_cold_startup) {
static void JNI_TraceEvent_WebViewStartupFirstInstance(
JNIEnv* env,
jlong start_time_ms,
jlong duration_ms,
jboolean included_global_startup) {
#if BUILDFLAG(ENABLE_BASE_TRACING)
auto t = perfetto::Track::ThreadScoped(
reinterpret_cast<void*>(trace_event::GetNextGlobalTraceId()));
if (is_cold_startup) {
TRACE_EVENT_BEGIN("android_webview.timeline",
"WebView.Startup.CreationTime.Stage2.ProviderInit.Cold",
t, TimeTicks() + Milliseconds(start_time_ms));
if (included_global_startup) {
TRACE_EVENT_BEGIN(
"android_webview.timeline",
"WebView.Startup.CreationTime.FirstInstanceWithGlobalStartup", t,
TimeTicks() + Milliseconds(start_time_ms));
} else {
TRACE_EVENT_BEGIN("android_webview.timeline",
"WebView.Startup.CreationTime.Stage2.ProviderInit.Warm",
t, TimeTicks() + Milliseconds(start_time_ms));
TRACE_EVENT_BEGIN(
"android_webview.timeline",
"WebView.Startup.CreationTime.FirstInstanceWithoutGlobalStartup", t,
TimeTicks() + Milliseconds(start_time_ms));
}
TRACE_EVENT_END("android_webview.timeline", t,
@ -328,6 +331,20 @@ static void JNI_TraceEvent_WebViewStartupStage2(JNIEnv* env,
#endif // BUILDFLAG(ENABLE_BASE_TRACING)
}
static void JNI_TraceEvent_WebViewStartupNotFirstInstance(JNIEnv* env,
jlong start_time_ms,
jlong duration_ms) {
#if BUILDFLAG(ENABLE_BASE_TRACING)
auto t = perfetto::Track::ThreadScoped(
reinterpret_cast<void*>(trace_event::GetNextGlobalTraceId()));
TRACE_EVENT_BEGIN("android_webview.timeline",
"WebView.Startup.CreationTime.NotFirstInstance", t,
TimeTicks() + Milliseconds(start_time_ms));
TRACE_EVENT_END("android_webview.timeline", t,
TimeTicks() + Milliseconds(start_time_ms + duration_ms));
#endif // BUILDFLAG(ENABLE_BASE_TRACING)
}
static void JNI_TraceEvent_WebViewStartupStartChromiumLocked(
JNIEnv* env,
jlong start_time_ms,

@ -7437,6 +7437,41 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram
name="Android.WebView.Startup.CreationTime.FirstInstanceAfterGlobalStartup"
units="ms" expires_after="2025-12-31">
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize the first WebViewChromium instance in the
case where we have already finished the process-global browser startup:
(&quot;Android.WebView.Startup.CreationTime.StartChromiumLocked&quot;) was
already complete and did not need to be performed during the instance
initialization.
This is recorded separately as it is significantly faster than the
&quot;FirstInstanceWithGlobalStartup&quot; case, but slower than the
&quot;NotFirstInstance&quot; case.
</summary>
</histogram>
<histogram
name="Android.WebView.Startup.CreationTime.FirstInstanceWithGlobalStartup"
units="ms" expires_after="2025-12-31">
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize the first WebViewChromium instance in the
case where we had to also do the process-global browser startup:
(&quot;Android.WebView.Startup.CreationTime.StartChromiumLocked&quot;) had
not yet run and was performed during the instance initialization.
This is recorded separately as it is significantly slower than the
&quot;FirstInstanceAfterGlobalStartup&quot; and &quot;NotFirstInstance&quot;
cases.
</summary>
</histogram>
<histogram name="Android.WebView.Startup.CreationTime.InitReason"
enum="WebViewStartupCallSite" expires_after="2025-12-31">
<owner>torne@chromium.org</owner>
@ -7450,13 +7485,41 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="Android.WebView.Startup.CreationTime.NotFirstInstance"
units="ms" expires_after="2025-12-31">
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize a subsequent WebViewChromium instance. This
will be recorded once for each instance created, other than the first to be
created in the current process.
This is recorded separately as it is significantly faster than the
&quot;FirstInstanceWithGlobalStartup&quot; and
&quot;FirstInstanceAfterGlobalStartup&quot; cases.
</summary>
</histogram>
<histogram name="Android.WebView.Startup.CreationTime.Stage1.FactoryInit"
units="ms" expires_after="2025-12-31">
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize a WebViewChromiumFactoryProvider. This is
the first major phase of the WebViewChromium construction.
How long it takes to run WebViewChromiumFactoryProvider.initialize(). This
does not count the time spent beforehand in WebViewFactory in the Android
framework, which first has to load the WebView code and do some basic setup
before initialize() can be called.
This will be recorded once per host process since the initialization is
process-global.
The &quot;Android.WebView.Startup.CreationTime.TotalFactoryInitTime&quot;
histogram measures the entire duration from when
WebViewFactory.getProvider() is first called until the
WebViewChromiumFactoryProvider is fully initialized and should be preferred
for most purposes, but is only recorded on Android S+ devices, so this older
metric is retained for now to allow &quot;fair&quot; comparisons with
Android R and earlier.
</summary>
</histogram>
@ -7465,10 +7528,23 @@ chromium-metrics-reviews@google.com.
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize a WebViewProvider, the first time that one
is initialized. WebViewProvider initialization is the second major phase of
WebViewChromium construction. The first initialization is recorded
separately because it is usually much slower than subsequent ones.
How long it takes to initialize a WebViewChromium instance in the case where
we had to also do the process-global browser startup:
(&quot;Android.WebView.Startup.CreationTime.StartChromiumLocked&quot;) had
not yet run and was performed during the instance initialization.
This is recorded separately as it is significantly slower than the
&quot;Warm&quot; case, and can only occur the first time that a
WebViewChromium instance is created. However, it's possible for browser
startup to already be complete before the first WebViewChromium instance is
created, in which case the &quot;Cold&quot; case will not be recorded during
this session.
The newer histograms FirstInstanceAfterGlobalStartup,
FirstInstanceWithGlobalStartup, and NotFirstInstance should be preferred for
most purposes as they correctly reflect that there are three distinct cases
here rather than only two, but the older histograms are retained for now to
allow &quot;fair&quot; comparisons with builds that predate the newer ones.
</summary>
</histogram>
@ -7477,10 +7553,24 @@ chromium-metrics-reviews@google.com.
<owner>torne@chromium.org</owner>
<owner>src/android_webview/OWNERS</owner>
<summary>
How long it takes to initialize a WebViewProvider, when it is initialized
the second or later times. WebViewProvider initialization is the second
major phase of WebViewChromium construction. When it is not the first time,
it is faster and thus recorded separately.
How long it takes to initialize a WebViewChromium instance in the case where
we have already finished the process-global browser startup:
(&quot;Android.WebView.Startup.CreationTime.StartChromiumLocked&quot;) was
already complete and did not need to be performed during the instance
initialization.
This is recorded separately as it is significantly faster than the
&quot;Cold&quot; case. However, it's possible for browser startup
initialization to already be complete before the first WebViewChromium
instance is created, in which case the first instance will be recorded as a
&quot;Warm&quot; case, even though it is expected to be slower than
subsequent instances, resulting in this metric being bimodal.
The newer histograms FirstInstanceAfterGlobalStartup,
FirstInstanceWithGlobalStartup, and NotFirstInstance should be preferred for
most purposes as they correctly reflect that there are three distinct cases
here rather than only two, but the older histograms are retained for now to
allow &quot;fair&quot; comparisons with builds that predate the newer ones.
</summary>
</histogram>