Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cobalt/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,11 @@ test("cobalt_unittests") {
}

if (!is_android) {
sources += [ "//cobalt/shell/browser/splash_screen_unittest.cc" ]
sources += [
"//cobalt/shell/browser/lifecycle_unittest.cc",
"//cobalt/shell/browser/shell_test_support.h",
"//cobalt/shell/browser/splash_screen_unittest.cc",
]
}

public_deps = [ "//third_party/zlib/google:compression_utils" ]
Expand Down
20 changes: 6 additions & 14 deletions cobalt/app/cobalt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,16 @@ int InitCobalt(int argc, const char** argv, const char* initial_deep_link) {

void SbEventHandle(const SbEvent* event) {
switch (event->type) {
case kSbEventTypePreload: {
#if BUILDFLAG(IS_COBALT_HERMETIC_BUILD)
init_musl();
#endif
SbEventStartData* data = static_cast<SbEventStartData*>(event->data);
g_exit_manager = new base::AtExitManager();
g_content_main_delegate = new cobalt::CobaltMainDelegate();
g_platform_event_source = new PlatformEventSourceStarboard();
InitCobalt(data->argument_count,
const_cast<const char**>(data->argument_values), data->link);

break;
}
case kSbEventTypePreload:
case kSbEventTypeStart: {
#if BUILDFLAG(IS_COBALT_HERMETIC_BUILD)
init_musl();
#endif
SbEventStartData* data = static_cast<SbEventStartData*>(event->data);
g_exit_manager = new base::AtExitManager();
g_content_main_delegate = new cobalt::CobaltMainDelegate();
g_content_main_delegate =
new cobalt::CobaltMainDelegate(false /* is_content_browsertests */,
event->type == kSbEventTypeStart);
g_platform_event_source = new PlatformEventSourceStarboard();
InitCobalt(data->argument_count,
const_cast<const char**>(data->argument_values), data->link);
Expand Down Expand Up @@ -174,7 +164,9 @@ void SbEventHandle(const SbEvent* event) {
break;
}
case kSbEventTypeConceal:
break;
case kSbEventTypeReveal:
content::Shell::OnReveal();
break;
case kSbEventTypeFreeze: {
auto* client = cobalt::CobaltContentBrowserClient::Get();
Expand Down
7 changes: 5 additions & 2 deletions cobalt/app/cobalt_main_delegate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@

namespace cobalt {

CobaltMainDelegate::CobaltMainDelegate() : content::ShellMainDelegate() {
CobaltMainDelegate::CobaltMainDelegate(bool is_content_browsertests,
bool is_visible)
: content::ShellMainDelegate(), is_visible_(is_visible) {
is_content_browsertests_ = is_content_browsertests;
CHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

Expand All @@ -49,7 +52,7 @@ std::optional<int> CobaltMainDelegate::BasicStartupComplete() {
content::ContentBrowserClient*
CobaltMainDelegate::CreateContentBrowserClient() {
CHECK_CALLED_ON_VALID_THREAD(thread_checker_);
browser_client_ = std::make_unique<CobaltContentBrowserClient>();
browser_client_ = std::make_unique<CobaltContentBrowserClient>(is_visible_);
return browser_client_.get();
}

Expand Down
4 changes: 3 additions & 1 deletion cobalt/app/cobalt_main_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ namespace cobalt {

class CobaltMainDelegate : public content::ShellMainDelegate {
public:
explicit CobaltMainDelegate();
explicit CobaltMainDelegate(bool is_content_browsertests = false,
bool is_visible = true);

CobaltMainDelegate(const CobaltMainDelegate&) = delete;
CobaltMainDelegate& operator=(const CobaltMainDelegate&) = delete;
Expand All @@ -56,6 +57,7 @@ class CobaltMainDelegate : public content::ShellMainDelegate {
~CobaltMainDelegate() override;

private:
bool is_visible_;
std::unique_ptr<content::BrowserMainRunner> main_runner_;
std::unique_ptr<CobaltContentBrowserClient> browser_client_;
std::unique_ptr<CobaltContentGpuClient> gpu_client_;
Expand Down
3 changes: 3 additions & 0 deletions cobalt/browser/cobalt_browser_main_parts.cc
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ void InitializeBrowserMemoryInstrumentationClient() {

} // namespace

CobaltBrowserMainParts::CobaltBrowserMainParts(bool is_visible)
: ShellBrowserMainParts(is_visible) {}

int CobaltBrowserMainParts::PreCreateThreads() {
SetupMetrics();

Expand Down
2 changes: 1 addition & 1 deletion cobalt/browser/cobalt_browser_main_parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class GlobalFeatures;
// ShellContentBrowserClient, this should implement BrowserMainParts.
class CobaltBrowserMainParts : public content::ShellBrowserMainParts {
public:
CobaltBrowserMainParts() = default;
explicit CobaltBrowserMainParts(bool is_visible = true);

CobaltBrowserMainParts(const CobaltBrowserMainParts&) = delete;
CobaltBrowserMainParts& operator=(const CobaltBrowserMainParts&) = delete;
Expand Down
8 changes: 5 additions & 3 deletions cobalt/browser/cobalt_content_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,9 @@ blink::UserAgentMetadata GetCobaltUserAgentMetadata() {
return metadata;
}

CobaltContentBrowserClient::CobaltContentBrowserClient()
: video_geometry_setter_service_(
CobaltContentBrowserClient::CobaltContentBrowserClient(bool is_visible)
: is_visible_(is_visible),
video_geometry_setter_service_(
std::unique_ptr<cobalt::media::VideoGeometrySetterService,
base::OnTaskRunnerDeleter>(
nullptr,
Expand Down Expand Up @@ -201,7 +202,8 @@ std::unique_ptr<content::BrowserMainParts>
CobaltContentBrowserClient::CreateBrowserMainParts(
bool /* is_integration_test */) {
CHECK_CALLED_ON_VALID_THREAD(thread_checker_);
auto browser_main_parts = std::make_unique<CobaltBrowserMainParts>();
auto browser_main_parts =
std::make_unique<CobaltBrowserMainParts>(is_visible_);
set_browser_main_parts(browser_main_parts.get());
return browser_main_parts;
}
Expand Down
3 changes: 2 additions & 1 deletion cobalt/browser/cobalt_content_browser_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class CobaltWebContentsObserver;
// a demo around Content.
class CobaltContentBrowserClient : public content::ShellContentBrowserClient {
public:
CobaltContentBrowserClient();
explicit CobaltContentBrowserClient(bool is_visible = true);

CobaltContentBrowserClient(const CobaltContentBrowserClient&) = delete;
CobaltContentBrowserClient& operator=(const CobaltContentBrowserClient&) =
Expand Down Expand Up @@ -152,6 +152,7 @@ class CobaltContentBrowserClient : public content::ShellContentBrowserClient {
void DispatchEvent(const std::string&, base::OnceClosure);
void OnSbWindowCreated(SbWindow window);

bool is_visible_;
std::unique_ptr<CobaltWebContentsObserver> web_contents_observer_;
std::unique_ptr<media::VideoGeometrySetterService, base::OnTaskRunnerDeleter>
video_geometry_setter_service_;
Expand Down
104 changes: 104 additions & 0 deletions cobalt/doc/preload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Application Preload

Preloading allows Cobalt to start and initialize in the background without
displaying any user interface. This enables a "background-to-foreground"
transition that appears instantaneous to the user when they eventually choose to
launch the application.

## For Web Designers

When an application is preloaded, it starts in a **hidden** state. Standard Web
APIs correctly reflect this state:

- `document.visibilityState` will be `"hidden"`.
- `document.hidden` will be `true`.

### Best Practices

- Avoid starting audio playback or heavy graphical animations while in the
hidden state.
- Listen for the `visibilitychange` event on the `document` object to detect
when the application transitions from preloaded to visible.

## For Device Manufacturers (Starboard Porters)

Preloading is managed through the Starboard lifecycle.

- **Startup:** To start in preload mode, the Starboard implementation should
send the `kSbEventTypePreload` event instead of `kSbEventTypeStart`.
- In the shared Starboard application framework (`starboard/shared/starboard/application.cc`),
the `--preload` command-line flag is recognized via the `kPreloadSwitch` constant.
- If a platform implementation's `Application::IsPreloadImmediate()` returns true
(typically by checking `HasPreloadSwitch()`), the application will
automatically call `DispatchPreload()`.
- `DispatchPreload()` then creates and dispatches the `kSbEventTypePreload`
initial event, which is the signal to the application that it should
initialize in a hidden state.
- **Revelation:** To bring a preloaded application to the foreground, the
platform should send a `kSbEventTypeReveal` signal.
- On Linux-based platforms, this is often triggered by sending a `SIGCONT`
signal to the process.
- An example of this mapping can be found in `starboard/shared/signal/suspend_signals.cc`,
where `SIGCONT` is handled by requesting a focus change.
- The shared Starboard application logic in `starboard/shared/starboard/application.cc`
automatically injects a `kSbEventTypeReveal` event if a focus request is
received while the application is in the preloaded (concealed) state.
- **Resource Management:** Cobalt defers the creation of the native window and
associated graphics resources until the first `Reveal` signal is received.
This minimizes the memory and CPU footprint of the application while it
resides in the background.
- **Splash Screen:** The creation of the splash screen's `WebContents` is also
skipped when the application is preloaded, further reducing the background
footprint.

## For Cobalt Developers

The "preload" signal is converted into a generic "visibility" state as soon as
it enters the application layer.

### Implementation Flow

1. **Entry Point:** `SbEventHandle` (in `cobalt/app/cobalt.cc`) receives
`kSbEventTypePreload`.
2. **State Propagation:** An `is_visible` boolean (set to `false`) is passed
through the constructor chain: `CobaltMainDelegate` ->
`CobaltContentBrowserClient` -> `CobaltBrowserMainParts` ->
`ShellBrowserMainParts`.
3. **Initialization:** `ShellBrowserMainParts::PreMainMessageLoopRun` calls
`Shell::Initialize`, passing the visibility state.
4. **Splash Screen Skip:** `Shell::CreateNewWindow` checks the visibility state
and skips creating the splash screen `WebContents` if the application is
initially hidden.
5. **Platform Delegate:** `Shell::Initialize` passes the state to
`ShellPlatformDelegate::Initialize`. Each platform implementation (e.g.,
Aura, Views) stores this in a member variable.
6. **Deferred Resource Creation:** `CreatePlatformWindow` checks `IsVisible()`
(or `IsConcealed()`) and defers creating the `NativeWindow` and `Widget` if
the application is not yet visible.
7. **Revelation:** When `kSbEventTypeReveal` is received, `Shell::OnReveal()` is
triggered. This calls `ShellPlatformDelegate::RevealShell`, which creates
the deferred window resources and calls `WasShown()` on the `WebContents`,
triggering the `visibilitychange` event for the web application.

### Unit Testing

Application lifecycle and visibility transitions are covered by the following
unit tests in the `cobalt_unittests` binary:

- **`LifecycleTest`** (`cobalt/shell/browser/lifecycle_unittest.cc`): Verifies
correct window creation and visibility state propagation during startup,
revelation, and redundant signals.
- **`SplashScreenTest`** (`cobalt/shell/browser/splash_screen_unittest.cc`):
Includes tests for ensuring the splash screen is skipped during preloading.

### Integration Testing

A robust integration test is provided in `cobalt/tools/test_preload.sh`. This
test:

1. Launches Cobalt in preload mode.
2. Uses the Chrome DevTools Protocol (CDP) to verify that
`document.visibilityState` is initially `"hidden"`.
3. Sends a `SIGCONT` signal to reveal the application.
4. Verifies via CDP that `document.visibilityState` transitions to `"visible"`.
5. Sends a `SIGPWR` signal to verify clean shutdown.
110 changes: 110 additions & 0 deletions cobalt/shell/browser/lifecycle_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2026 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "cobalt/shell/browser/shell.h"
#include "cobalt/shell/browser/shell_test_support.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;

namespace content {

class LifecycleTest : public ShellTestBase {
public:
LifecycleTest() = default;

Shell* CreateTestShell(bool is_visible) {
InitializeShell(is_visible);
WebContents::CreateParams create_params(browser_context_.get());
create_params.desired_renderer_state =
WebContents::CreateParams::kNoRendererProcess;
create_params.initially_hidden = !is_visible;
std::unique_ptr<WebContents> web_contents(
TestWebContents::Create(create_params));

Shell* shell =
new Shell(std::move(web_contents), nullptr /* splash_contents */,
/*should_set_delegate=*/true,
/*topic*/ "",
/*skip_for_testing=*/true);
if (is_visible) {
EXPECT_CALL(*platform_, CreatePlatformWindow(shell, _));
Shell::GetPlatform()->CreatePlatformWindow(shell, gfx::Size());
}
EXPECT_CALL(*platform_, SetContents(shell));
Shell::FinishShellInitialization(shell);
return shell;
}
};

TEST_F(LifecycleTest, StartupVisible) {
Shell* shell = CreateTestShell(true /* is_visible */);

ASSERT_NE(shell->web_contents(), nullptr);
EXPECT_TRUE(platform_->IsVisible());
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);

EXPECT_CALL(*platform_, DestroyShell(shell));
shell->Close();
}

TEST_F(LifecycleTest, StartupHidden) {
Shell* shell = CreateTestShell(false /* is_visible */);

ASSERT_NE(shell->web_contents(), nullptr);
EXPECT_FALSE(platform_->IsVisible());
// Preloading (starting hidden) should result in HIDDEN visibility.
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::HIDDEN);

EXPECT_CALL(*platform_, DestroyShell(shell));
shell->Close();
}

TEST_F(LifecycleTest, Reveal) {
Shell* shell = CreateTestShell(false /* is_visible */);

EXPECT_FALSE(platform_->IsVisible());
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::HIDDEN);

// Trigger reveal.
EXPECT_CALL(*platform_, RevealShell(shell));
Shell::OnReveal();

EXPECT_TRUE(platform_->IsVisible());
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);

EXPECT_CALL(*platform_, DestroyShell(shell));
shell->Close();
}

TEST_F(LifecycleTest, RedundantReveal) {
Shell* shell = CreateTestShell(false /* is_visible */);

// First reveal.
EXPECT_CALL(*platform_, RevealShell(shell)).Times(1);
Shell::OnReveal();

// Redundant reveal should do nothing.
EXPECT_CALL(*platform_, RevealShell(_)).Times(0);
Shell::OnReveal();

EXPECT_TRUE(platform_->IsVisible());
EXPECT_EQ(shell->web_contents()->GetVisibility(), Visibility::VISIBLE);

EXPECT_CALL(*platform_, DestroyShell(shell));
shell->Close();
}

} // namespace content
Loading
Loading