Skip to content

Commit 97ca4ee

Browse files
Implement basic application preload functionality. (#8928)
Implement background preloading support for instantaneous application launching. This feature allows Cobalt to initialize in a hidden state, deferring heavy resource creation until the application is revealed to the foreground. To further minimize footprint, the splash screen creation is skipped entirely during preloading. - Support starting Cobalt in a background/hidden state via the kSbEventTypePreload signal. - Defer creation of native windows and UI widgets until the application is revealed, minimizing initial memory and CPU footprint. - Skip creation of splash screen WebContents when preloaded to further reduce background resource usage. - Handle kSbEventTypeReveal events to transition the application from a background to a visible state and initialize deferred UI components. - Ensure standard Web APIs (document.visibilityState, document.hidden) correctly reflect the application's hidden state during preloading and its revealed state. - Standardize on visibility state propagation throughout the browser initialization chain (MainDelegate, BrowserClient, MainParts), keeping "preload" logic encapsulated at the Starboard entry point. - Consolidate window creation logic into a private internal helper to ensure consistent initialization for both normal and preloaded startups. - Refactor shell unit tests into a reusable ShellTestBase and extract common mock infrastructure into shell_test_support.h. - Add comprehensive unit tests: - LifecycleTest: Verifies startup states, revelation, and signal idempotency. - SplashScreenTest: Verifies that splash screen is skipped during preloading. - Add a comprehensive automated integration test (test_preload.sh) to verify preloading, revelation, and clean shutdown. - Add detailed documentation for developers, device manufacturers, and web designers in cobalt/doc/preload.md. - Enhanced developer tools with usage examples and captured test output. Bug: 447660888 --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent dacb637 commit 97ca4ee

24 files changed

+835
-174
lines changed

cobalt/BUILD.gn

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,11 @@ test("cobalt_unittests") {
205205
}
206206

207207
if (!is_android) {
208-
sources += [ "//cobalt/shell/browser/splash_screen_unittest.cc" ]
208+
sources += [
209+
"//cobalt/shell/browser/lifecycle_unittest.cc",
210+
"//cobalt/shell/browser/shell_test_support.h",
211+
"//cobalt/shell/browser/splash_screen_unittest.cc",
212+
]
209213
}
210214

211215
public_deps = [ "//third_party/zlib/google:compression_utils" ]

cobalt/app/cobalt.cc

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,16 @@ int InitCobalt(int argc, const char** argv, const char* initial_deep_link) {
107107

108108
void SbEventHandle(const SbEvent* event) {
109109
switch (event->type) {
110-
case kSbEventTypePreload: {
111-
#if BUILDFLAG(IS_COBALT_HERMETIC_BUILD)
112-
init_musl();
113-
#endif
114-
SbEventStartData* data = static_cast<SbEventStartData*>(event->data);
115-
g_exit_manager = new base::AtExitManager();
116-
g_content_main_delegate = new cobalt::CobaltMainDelegate();
117-
g_platform_event_source = new PlatformEventSourceStarboard();
118-
InitCobalt(data->argument_count,
119-
const_cast<const char**>(data->argument_values), data->link);
120-
121-
break;
122-
}
110+
case kSbEventTypePreload:
123111
case kSbEventTypeStart: {
124112
#if BUILDFLAG(IS_COBALT_HERMETIC_BUILD)
125113
init_musl();
126114
#endif
127115
SbEventStartData* data = static_cast<SbEventStartData*>(event->data);
128116
g_exit_manager = new base::AtExitManager();
129-
g_content_main_delegate = new cobalt::CobaltMainDelegate();
117+
g_content_main_delegate =
118+
new cobalt::CobaltMainDelegate(false /* is_content_browsertests */,
119+
event->type == kSbEventTypeStart);
130120
g_platform_event_source = new PlatformEventSourceStarboard();
131121
InitCobalt(data->argument_count,
132122
const_cast<const char**>(data->argument_values), data->link);
@@ -160,7 +150,10 @@ void SbEventHandle(const SbEvent* event) {
160150
g_platform_event_source->HandleFocusEvent(event);
161151
break;
162152
case kSbEventTypeConceal:
153+
break;
163154
case kSbEventTypeReveal:
155+
content::Shell::OnReveal();
156+
break;
164157
case kSbEventTypeFreeze:
165158
case kSbEventTypeUnfreeze:
166159
break;

cobalt/app/cobalt_main_delegate.cc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@
3030

3131
namespace cobalt {
3232

33-
CobaltMainDelegate::CobaltMainDelegate(bool is_content_browsertests)
34-
: content::ShellMainDelegate(is_content_browsertests) {}
33+
CobaltMainDelegate::CobaltMainDelegate(bool is_content_browsertests,
34+
bool is_visible)
35+
: content::ShellMainDelegate(is_content_browsertests),
36+
is_visible_(is_visible) {}
3537

3638
CobaltMainDelegate::~CobaltMainDelegate() {}
37-
3839
absl::optional<int> CobaltMainDelegate::BasicStartupComplete() {
3940
base::CommandLine* cl = base::CommandLine::ForCurrentProcess();
4041
cl->AppendSwitch(switches::kEnableAggressiveDOMStorageFlushing);
@@ -45,7 +46,7 @@ absl::optional<int> CobaltMainDelegate::BasicStartupComplete() {
4546

4647
content::ContentBrowserClient*
4748
CobaltMainDelegate::CreateContentBrowserClient() {
48-
browser_client_ = std::make_unique<CobaltContentBrowserClient>();
49+
browser_client_ = std::make_unique<CobaltContentBrowserClient>(is_visible_);
4950
return browser_client_.get();
5051
}
5152

cobalt/app/cobalt_main_delegate.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ namespace cobalt {
2828

2929
class CobaltMainDelegate : public content::ShellMainDelegate {
3030
public:
31-
explicit CobaltMainDelegate(bool is_content_browsertests = false);
31+
explicit CobaltMainDelegate(bool is_content_browsertests = false,
32+
bool is_visible = true);
3233

3334
CobaltMainDelegate(const CobaltMainDelegate&) = delete;
3435
CobaltMainDelegate& operator=(const CobaltMainDelegate&) = delete;
@@ -54,6 +55,7 @@ class CobaltMainDelegate : public content::ShellMainDelegate {
5455
~CobaltMainDelegate() override;
5556

5657
private:
58+
bool is_visible_;
5759
std::unique_ptr<content::BrowserMainRunner> main_runner_;
5860
std::unique_ptr<CobaltContentBrowserClient> browser_client_;
5961
std::unique_ptr<CobaltContentGpuClient> gpu_client_;

cobalt/browser/cobalt_browser_main_parts.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838

3939
namespace cobalt {
4040

41+
CobaltBrowserMainParts::CobaltBrowserMainParts(bool is_visible)
42+
: ShellBrowserMainParts(is_visible) {}
43+
4144
int CobaltBrowserMainParts::PreCreateThreads() {
4245
SetupMetrics();
4346
#if BUILDFLAG(IS_ANDROIDTV)

cobalt/browser/cobalt_browser_main_parts.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class GlobalFeatures;
4141
// ShellContentBrowserClient, this should implement BrowserMainParts.
4242
class CobaltBrowserMainParts : public content::ShellBrowserMainParts {
4343
public:
44-
CobaltBrowserMainParts() = default;
44+
explicit CobaltBrowserMainParts(bool is_visible = true);
4545

4646
CobaltBrowserMainParts(const CobaltBrowserMainParts&) = delete;
4747
CobaltBrowserMainParts& operator=(const CobaltBrowserMainParts&) = delete;

cobalt/browser/cobalt_content_browser_client.cc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,9 @@ blink::UserAgentMetadata GetCobaltUserAgentMetadata() {
155155
return metadata;
156156
}
157157

158-
CobaltContentBrowserClient::CobaltContentBrowserClient()
159-
: video_geometry_setter_service_(
158+
CobaltContentBrowserClient::CobaltContentBrowserClient(bool is_visible)
159+
: is_visible_(is_visible),
160+
video_geometry_setter_service_(
160161
std::unique_ptr<cobalt::media::VideoGeometrySetterService,
161162
base::OnTaskRunnerDeleter>(
162163
nullptr,
@@ -186,7 +187,8 @@ std::unique_ptr<content::BrowserMainParts>
186187
CobaltContentBrowserClient::CreateBrowserMainParts(
187188
bool /* is_integration_test */) {
188189
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
189-
auto browser_main_parts = std::make_unique<CobaltBrowserMainParts>();
190+
auto browser_main_parts =
191+
std::make_unique<CobaltBrowserMainParts>(is_visible_);
190192
set_browser_main_parts(browser_main_parts.get());
191193
return browser_main_parts;
192194
}

cobalt/browser/cobalt_content_browser_client.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class CobaltWebContentsObserver;
6262
// a demo around Content.
6363
class CobaltContentBrowserClient : public content::ShellContentBrowserClient {
6464
public:
65-
CobaltContentBrowserClient();
65+
explicit CobaltContentBrowserClient(bool is_visible = true);
6666

6767
CobaltContentBrowserClient(const CobaltContentBrowserClient&) = delete;
6868
CobaltContentBrowserClient& operator=(const CobaltContentBrowserClient&) =
@@ -147,6 +147,7 @@ class CobaltContentBrowserClient : public content::ShellContentBrowserClient {
147147
void CreateVideoGeometrySetterService();
148148
void OnSbWindowCreated(SbWindow window);
149149

150+
bool is_visible_;
150151
std::unique_ptr<CobaltWebContentsObserver> web_contents_observer_;
151152
std::unique_ptr<media::VideoGeometrySetterService, base::OnTaskRunnerDeleter>
152153
video_geometry_setter_service_;

cobalt/doc/preload.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Application Preload
2+
3+
Preloading allows Cobalt to start and initialize in the background without
4+
displaying any user interface. This enables a "background-to-foreground"
5+
transition that appears instantaneous to the user when they eventually choose to
6+
launch the application.
7+
8+
## For Web Designers
9+
10+
When an application is preloaded, it starts in a **hidden** state. Standard Web
11+
APIs correctly reflect this state:
12+
13+
- `document.visibilityState` will be `"hidden"`.
14+
- `document.hidden` will be `true`.
15+
16+
### Best Practices
17+
18+
- Avoid starting audio playback or heavy graphical animations while in the
19+
hidden state.
20+
- Listen for the `visibilitychange` event on the `document` object to detect
21+
when the application transitions from preloaded to visible.
22+
23+
## For Device Manufacturers (Starboard Porters)
24+
25+
Preloading is managed through the Starboard lifecycle.
26+
27+
- **Startup:** To start in preload mode, the Starboard implementation should
28+
send the `kSbEventTypePreload` event instead of `kSbEventTypeStart`.
29+
- In the shared Starboard application framework (`starboard/shared/starboard/application.cc`),
30+
the `--preload` command-line flag is recognized via the `kPreloadSwitch` constant.
31+
- If a platform implementation's `Application::IsPreloadImmediate()` returns true
32+
(typically by checking `HasPreloadSwitch()`), the application will
33+
automatically call `DispatchPreload()`.
34+
- `DispatchPreload()` then creates and dispatches the `kSbEventTypePreload`
35+
initial event, which is the signal to the application that it should
36+
initialize in a hidden state.
37+
- **Revelation:** To bring a preloaded application to the foreground, the
38+
platform should send a `kSbEventTypeReveal` signal.
39+
- On Linux-based platforms, this is often triggered by sending a `SIGCONT`
40+
signal to the process.
41+
- An example of this mapping can be found in `starboard/shared/signal/suspend_signals.cc`,
42+
where `SIGCONT` is handled by requesting a focus change.
43+
- The shared Starboard application logic in `starboard/shared/starboard/application.cc`
44+
automatically injects a `kSbEventTypeReveal` event if a focus request is
45+
received while the application is in the preloaded (concealed) state.
46+
- **Resource Management:** Cobalt defers the creation of the native window and
47+
associated graphics resources until the first `Reveal` signal is received.
48+
This minimizes the memory and CPU footprint of the application while it
49+
resides in the background.
50+
- **Splash Screen:** The creation of the splash screen's `WebContents` is also
51+
skipped when the application is preloaded, further reducing the background
52+
footprint.
53+
54+
## For Cobalt Developers
55+
56+
The "preload" signal is converted into a generic "visibility" state as soon as
57+
it enters the application layer.
58+
59+
### Implementation Flow
60+
61+
1. **Entry Point:** `SbEventHandle` (in `cobalt/app/cobalt.cc`) receives
62+
`kSbEventTypePreload`.
63+
2. **State Propagation:** An `is_visible` boolean (set to `false`) is passed
64+
through the constructor chain: `CobaltMainDelegate` ->
65+
`CobaltContentBrowserClient` -> `CobaltBrowserMainParts` ->
66+
`ShellBrowserMainParts`.
67+
3. **Initialization:** `ShellBrowserMainParts::PreMainMessageLoopRun` calls
68+
`Shell::Initialize`, passing the visibility state.
69+
4. **Splash Screen Skip:** `Shell::CreateNewWindow` checks the visibility state
70+
and skips creating the splash screen `WebContents` if the application is
71+
initially hidden.
72+
5. **Platform Delegate:** `Shell::Initialize` passes the state to
73+
`ShellPlatformDelegate::Initialize`. Each platform implementation (e.g.,
74+
Aura, Views) stores this in a member variable.
75+
6. **Deferred Resource Creation:** `CreatePlatformWindow` checks `IsVisible()`
76+
(or `IsConcealed()`) and defers creating the `NativeWindow` and `Widget` if
77+
the application is not yet visible.
78+
7. **Revelation:** When `kSbEventTypeReveal` is received, `Shell::OnReveal()` is
79+
triggered. This calls `ShellPlatformDelegate::RevealShell`, which creates
80+
the deferred window resources and calls `WasShown()` on the `WebContents`,
81+
triggering the `visibilitychange` event for the web application.
82+
83+
### Unit Testing
84+
85+
Application lifecycle and visibility transitions are covered by the following
86+
unit tests in the `cobalt_unittests` binary:
87+
88+
- **`LifecycleTest`** (`cobalt/shell/browser/lifecycle_unittest.cc`): Verifies
89+
correct window creation and visibility state propagation during startup,
90+
revelation, and redundant signals.
91+
- **`SplashScreenTest`** (`cobalt/shell/browser/splash_screen_unittest.cc`):
92+
Includes tests for ensuring the splash screen is skipped during preloading.
93+
94+
### Integration Testing
95+
96+
A robust integration test is provided in `cobalt/tools/test_preload.sh`. This
97+
test:
98+
99+
1. Launches Cobalt in preload mode.
100+
2. Uses the Chrome DevTools Protocol (CDP) to verify that
101+
`document.visibilityState` is initially `"hidden"`.
102+
3. Sends a `SIGCONT` signal to reveal the application.
103+
4. Verifies via CDP that `document.visibilityState` transitions to `"visible"`.
104+
5. Sends a `SIGPWR` signal to verify clean shutdown.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2026 The Cobalt Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "cobalt/shell/browser/shell.h"
16+
#include "cobalt/shell/browser/shell_test_support.h"
17+
#include "content/test/test_web_contents.h"
18+
#include "testing/gtest/include/gtest/gtest.h"
19+
20+
using testing::_;
21+
22+
namespace content {
23+
24+
class LifecycleTest : public ShellTestBase {
25+
public:
26+
LifecycleTest() = default;
27+
28+
Shell* CreateTestShell(bool is_visible) {
29+
InitializeShell(is_visible);
30+
WebContents::CreateParams create_params(browser_context_.get());
31+
create_params.desired_renderer_state =
32+
WebContents::CreateParams::kNoRendererProcess;
33+
create_params.initially_hidden = !is_visible;
34+
std::unique_ptr<WebContents> web_contents(
35+
TestWebContents::Create(create_params));
36+
37+
Shell* shell =
38+
new Shell(std::move(web_contents), nullptr /* splash_contents */,
39+
/*should_set_delegate=*/true,
40+
/*topic*/ "",
41+
/*skip_for_testing=*/true);
42+
if (is_visible) {
43+
EXPECT_CALL(*platform_, CreatePlatformWindow(shell, _));
44+
Shell::GetPlatform()->CreatePlatformWindow(shell, gfx::Size());
45+
}
46+
EXPECT_CALL(*platform_, SetContents(shell));
47+
Shell::FinishShellInitialization(shell);
48+
return shell;
49+
}
50+
};
51+
52+
TEST_F(LifecycleTest, StartupVisible) {
53+
Shell* shell = CreateTestShell(true /* is_visible */);
54+
55+
ASSERT_NE(shell->web_contents(), nullptr);
56+
EXPECT_TRUE(platform_->IsVisible());
57+
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);
58+
59+
EXPECT_CALL(*platform_, DestroyShell(shell));
60+
shell->Close();
61+
}
62+
63+
TEST_F(LifecycleTest, StartupHidden) {
64+
Shell* shell = CreateTestShell(false /* is_visible */);
65+
66+
ASSERT_NE(shell->web_contents(), nullptr);
67+
EXPECT_FALSE(platform_->IsVisible());
68+
// Preloading (starting hidden) should result in HIDDEN visibility.
69+
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::HIDDEN);
70+
71+
EXPECT_CALL(*platform_, DestroyShell(shell));
72+
shell->Close();
73+
}
74+
75+
TEST_F(LifecycleTest, Reveal) {
76+
Shell* shell = CreateTestShell(false /* is_visible */);
77+
78+
EXPECT_FALSE(platform_->IsVisible());
79+
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::HIDDEN);
80+
81+
// Trigger reveal.
82+
EXPECT_CALL(*platform_, RevealShell(shell));
83+
Shell::OnReveal();
84+
85+
EXPECT_TRUE(platform_->IsVisible());
86+
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);
87+
88+
EXPECT_CALL(*platform_, DestroyShell(shell));
89+
shell->Close();
90+
}
91+
92+
TEST_F(LifecycleTest, RedundantReveal) {
93+
Shell* shell = CreateTestShell(false /* is_visible */);
94+
95+
// First reveal.
96+
EXPECT_CALL(*platform_, RevealShell(shell)).Times(1);
97+
Shell::OnReveal();
98+
99+
// Redundant reveal should do nothing.
100+
EXPECT_CALL(*platform_, RevealShell(_)).Times(0);
101+
Shell::OnReveal();
102+
103+
EXPECT_TRUE(platform_->IsVisible());
104+
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);
105+
106+
EXPECT_CALL(*platform_, DestroyShell(shell));
107+
shell->Close();
108+
}
109+
110+
} // namespace content

0 commit comments

Comments
 (0)