blob: c12fe5330a0159233a0da5b27266ea6f85c27c09 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <UIKit/UIKit.h>
#include "base/callback_helpers.h"
#import "base/mac/bind_objc_block.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/browser/snapshots/snapshot_manager.h"
#import "ios/chrome/browser/tabs/tab.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#include "ios/chrome/browser/test/perf_test_with_bvc_ios.h"
#import "ios/chrome/browser/ui/browser_view_controller.h"
#import "ios/chrome/browser/ui/fullscreen_controller.h"
#import "ios/chrome/browser/ui/stack_view/stack_view_controller.h"
#include "ios/chrome/browser/ui/ui_util.h"
#include "ios/web/public/referrer.h"
#import "net/base/mac/url_conversions.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// These tests measure the performance of opening the stack view controller on
// an iPhone. On an iPad, the tests do not run, as the iPad does not use the
// stack view controller.
// Opening the SVC smoothly and quickly is critical to the user experience, and
// any sort of pause or delay is awkward and gives a poor impression.
// The target time of opening SVC is less than 250 ms. In order to target
// optimizations and ensure that improvements actually lower the time, this test
// aims to mimic as closely as possible the application experience.
// To mimic the full experience, it is necessary to set up a browser view
// controller, with a website loaded, and use as much of the generic experience
// as possible while culling items such as PrerenderController which may
// muddy the performance analysis. In that vein, the tests wait for websites to
// load before opening the stack view controller, to avoid slowing down the
// animation run loops with networking or javascript calls. This way the tests
// provide meaningful and trackable performance numbers. The downside is that it
// means the tests do not completely mimic the user experience, as the animation
// may in fact take longer if the website is still loading, and the UIWebView
// making network calls on the main thread.
// Testing delegate to receive animation start and end notifications.
// It contains a weak pointer to the BrowserViewController so that it can
// take a snapshot of the toolbar for the toolbar animation.
@interface StackViewControllerPerfTestDelegate
: NSObject<TabSwitcherDelegate, StackViewControllerTestDelegate> {
@private
BOOL showAnimationStarted_;
BOOL showAnimationEnded_;
BOOL dismissAnimationStarted_;
BOOL dismissAnimationEnded_;
BOOL preloadCardViewsEnded_;
@public
__weak BrowserViewController* bvc_;
}
- (void)reinitialize;
@property(nonatomic, assign) BOOL showAnimationStarted;
@property(nonatomic, assign) BOOL showAnimationEnded;
@property(nonatomic, assign) BOOL dismissAnimationStarted;
@property(nonatomic, assign) BOOL dismissAnimationEnded;
@property(nonatomic, assign) BOOL preloadCardViewsEnded;
@end
@implementation StackViewControllerPerfTestDelegate
@synthesize showAnimationStarted = showAnimationStarted_;
@synthesize showAnimationEnded = showAnimationEnded_;
@synthesize dismissAnimationStarted = dismissAnimationStarted_;
@synthesize dismissAnimationEnded = dismissAnimationEnded_;
@synthesize preloadCardViewsEnded = preloadCardViewsEnded_;
- (id)initWithBrowserViewController:(BrowserViewController*)bvc {
self = [super init];
if (self)
bvc_ = bvc;
return self;
}
- (void)reinitialize {
[self setShowAnimationStarted:NO];
[self setShowAnimationEnded:NO];
[self setDismissAnimationStarted:NO];
[self setDismissAnimationEnded:NO];
[self setPreloadCardViewsEnded:NO];
}
- (void)stackViewControllerShowWithSelectedTabAnimationDidStart {
[self setShowAnimationStarted:YES];
}
- (void)stackViewControllerShowWithSelectedTabAnimationDidEnd {
[self setShowAnimationEnded:YES];
}
- (void)tabSwitcher:(id<TabSwitcher>)tabSwitcher
dismissTransitionWillStartWithActiveModel:(TabModel*)tabModel {
[self setDismissAnimationStarted:YES];
}
- (void)tabSwitcherDismissTransitionDidEnd:(id<TabSwitcher>)tabSwitcher {
[self setDismissAnimationEnded:YES];
}
- (void)stackViewControllerPreloadCardViewsDidEnd {
[self setPreloadCardViewsEnded:YES];
}
- (void)tabSwitcherPresentationTransitionDidEnd:(id<TabSwitcher>)tabSwitcher {
[self stackViewControllerShowWithSelectedTabAnimationDidEnd];
}
- (id<ToolbarOwner>)tabSwitcherTransitionToolbarOwner {
return bvc_;
}
@end
#pragma mark -
namespace {
#define ARRAYSIZE(a) \
((sizeof(a) / sizeof(*(a))) / \
static_cast<size_t>(!(sizeof(a) % sizeof(*(a)))))
// Use multiple URLs in the test in case the complexity of a website has an
// effect on the performance of UIWebView -renderInContext.
static const char* url_list[] = {
// TODO(crbug.com/546315): Create static websites for these.
"https://www.google.com", "https://news.google.com",
"https://browsingtest.appspot.com",
};
// The maximum delay of a single spin of the run loop in seconds.
// This is very small to ensure that we are spining fast enough.
const NSTimeInterval kSpinDelay = 0.01; // seconds
// The total maximum delay of all spins of the run loop while
// waiting for the stack view to appear and disappear.
const NSTimeInterval kTotalSpinDelay = 20; // seconds
// The maximum time to wait for a website to load.
const NSTimeInterval kMaxPageLoadDelay = 20; // seconds
// The time UI gets to catch up after a website has loaded. Give a full second
// to make sure the progress bar's finish delay has completed and the toolbar
// snapshot taken.
const NSTimeInterval kMaxUICatchupDelay = 1.0; // seconds
class StackViewControllerPerfTest : public PerfTestWithBVC {
public:
StackViewControllerPerfTest() : PerfTestWithBVC("Stack View") {}
void SetUp() override {
// Opening a StackViewController is done only on iPhones, not on iPads.
// This test is meaningless on an iPad.
if (IsIPadIdiom())
return;
// Base class does most of the setup.
PerfTestWithBVC::SetUp();
current_url_index_ = 0;
reuse_svc_ = false;
// The testing delegate will receive stack view animation notifications.
delegate_ = [[StackViewControllerPerfTestDelegate alloc]
initWithBrowserViewController:bvc_];
}
void TearDown() override {
// Opening a StackViewController is done only on iPhones, not on iPads.
// This test is meaningless on an iPad.
if (IsIPadIdiom())
return;
PerfTestWithBVC::TearDown();
}
protected:
// Stack view controller & delegate.
StackViewControllerPerfTestDelegate* delegate_;
StackViewController* view_controller_;
int current_url_index_;
BOOL reuse_svc_;
// Utility function to print out timing information for testing
// with |number_of_tabs| tabs opened.
void PrintTestResult(std::string test_description,
int number_of_tabs,
base::TimeDelta elapsed) {
std::stringstream test_name;
test_name << test_description << " - " << number_of_tabs << " Tab"
<< (number_of_tabs == 1 ? "" : "s");
LogPerfTiming(test_name.str(), elapsed);
}
// Creates and adds |number_of_tabs| tabs to the tab model. If |same_url|
// is true, always uses "www.google.com", otherwise iterates through url_list.
void CreateTabs(int number_of_tabs, bool same_url);
// Navigates to the next URL in the current tab.
void LoadNextURL();
// Gets the next URL in the list.
const GURL NextURL();
// Gets "google.com"
static const GURL GoogleURL();
// Waits for the page to load in the given tab.
static void WaitForPageLoad(Tab* tab);
// Creates the stack view and adds it to the main window.
base::TimeDelta OpenStackView();
// Copy of MainController's -showTabSwitcher function. Pulling in all of
// MainController is not practical for unit tests, nor necessary.
void MainControllerShowTabSwitcher();
// Dismisses the stack view and removes it from the main window.
base::TimeDelta CloseStackView();
// Time how long it takes BVC to make a snapshot of the current website.
base::TimeDelta TakeSnapshot();
};
void StackViewControllerPerfTest::CreateTabs(int number_of_tabs,
bool same_url) {
// Create and add the tabs to the tab model.
Tab* tab = nil;
for (int i = 0; i < number_of_tabs; i++) {
tab = [bvc_ addSelectedTabWithURL:(same_url ? GoogleURL() : NextURL())
transition:(ui::PAGE_TRANSITION_AUTO_TOPLEVEL)];
WaitForPageLoad(tab);
}
}
void StackViewControllerPerfTest::LoadNextURL() {
[bvc_ loadURL:NextURL()
referrer:web::Referrer()
transition:(ui::PAGE_TRANSITION_TYPED)
rendererInitiated:NO];
Tab* tab = [tab_model_ currentTab];
WaitForPageLoad(tab);
}
const GURL StackViewControllerPerfTest::NextURL() {
current_url_index_++;
if (static_cast<unsigned long>(current_url_index_) >= ARRAYSIZE(url_list))
current_url_index_ = 0;
return GURL(url_list[current_url_index_]);
}
const GURL StackViewControllerPerfTest::GoogleURL() {
return GURL("http://www.google.com");
}
void StackViewControllerPerfTest::WaitForPageLoad(Tab* tab) {
base::test::ios::WaitUntilCondition(
^bool() {
return !tab.webState->IsLoading();
},
false, base::TimeDelta::FromSecondsD(kMaxPageLoadDelay));
base::test::ios::WaitUntilCondition(
nil, false, base::TimeDelta::FromSecondsD(kMaxUICatchupDelay));
}
base::TimeDelta StackViewControllerPerfTest::OpenStackView() {
return base::test::ios::TimeUntilCondition(
^{
[delegate_ reinitialize];
MainControllerShowTabSwitcher();
},
^bool() {
return [delegate_ showAnimationEnded];
},
false, base::TimeDelta::FromSecondsD(kTotalSpinDelay));
}
void StackViewControllerPerfTest::MainControllerShowTabSwitcher() {
// The code for this function is copied from MainController -showTabSwitcher.
// Note that if the code there changes, this code should change to match.
Tab* currentTab = [[bvc_ tabModel] currentTab];
// In order to generate the transition between the current browser view
// controller and the tab switcher controller it's possible that multiple
// screenshots of the same tab are taken. Since taking a screenshot is
// expensive we activate snapshot coalescing in the scope of this function
// which will cache the first snapshot for the tab and reuse it instead of
// regenerating a new one each time.
[currentTab setSnapshotCoalescingEnabled:YES];
base::ScopedClosureRunner runner(base::BindBlockArc(^{
[currentTab setSnapshotCoalescingEnabled:NO];
}));
if (!view_controller_) {
view_controller_ =
[[StackViewController alloc] initWithMainTabModel:tab_model_
otrTabModel:otr_tab_model_
activeTabModel:tab_model_];
} else {
[view_controller_ restoreInternalStateWithMainTabModel:tab_model_
otrTabModel:otr_tab_model_
activeTabModel:tab_model_];
}
[view_controller_ setDelegate:delegate_];
// The only addition to the function for testing.
[view_controller_ setTestDelegate:delegate_];
[bvc_ presentViewController:view_controller_
animated:NO
completion:^{
[view_controller_ showWithSelectedTabAnimation];
EXPECT_TRUE([delegate_ showAnimationStarted]);
EXPECT_FALSE([delegate_ showAnimationEnded]);
}];
}
base::TimeDelta StackViewControllerPerfTest::CloseStackView() {
base::Time startTime = base::Time::NowFromSystemTime();
// Spin and wait for the dismiss stack view animation to finish.
base::test::ios::TimeUntilCondition(
^{
[view_controller_ dismissWithSelectedTabAnimation];
EXPECT_TRUE([delegate_ dismissAnimationStarted]);
EXPECT_FALSE([delegate_ dismissAnimationEnded]);
},
^bool() {
return [delegate_ dismissAnimationEnded];
},
false, base::TimeDelta::FromSecondsD(kTotalSpinDelay));
[view_controller_ dismissViewControllerAnimated:NO completion:nil];
if (!reuse_svc_)
view_controller_ = nil;
base::TimeDelta closeTime = base::Time::NowFromSystemTime() - startTime;
// Run the runloop a bit longer to give time for temporary retains that happen
// in the OS during view teardown to resolve, so that the view gets its
// dismissal callbacks.
base::test::ios::WaitUntilCondition(
nil, false, base::TimeDelta::FromSecondsD(kSpinDelay));
return closeTime;
}
base::TimeDelta StackViewControllerPerfTest::TakeSnapshot() {
base::Time startTime = base::Time::NowFromSystemTime();
UIImage* image = [[tab_model_ currentTab] updateSnapshotWithOverlay:YES
visibleFrameOnly:YES];
base::TimeDelta elapsed = base::Time::NowFromSystemTime() - startTime;
EXPECT_TRUE(image);
return elapsed;
}
TEST_F(StackViewControllerPerfTest, WebView_Shapshot) {
// Opening a StackViewController is done only on iPhones, not on iPads.
// This test is meaningless on an iPad.
if (IsIPadIdiom())
return;
const int kNumTests = 10;
base::TimeDelta times[kNumTests];
CreateTabs(1, false);
for (int i = 0; i < kNumTests; i++) {
times[i] = TakeSnapshot();
LoadNextURL();
}
base::TimeDelta avg = CalculateAverage(times, kNumTests, NULL, NULL);
LogPerfTiming("Snapshot", avg);
}
// TODO(crbug.com/546328): Add back in tests for checking opening & closing
// stack view controller with multiple tabs open.
TEST_F(StackViewControllerPerfTest, DISABLED_OpenAndCloseStackView_1_Tab) {
// Opening a StackViewController is done only on iPhones, not on iPads.
// This test is meaningless on an iPad.
if (IsIPadIdiom())
return;
reuse_svc_ = true;
const int kNumTests = 10;
base::TimeDelta open_times[kNumTests];
base::TimeDelta close_times[kNumTests];
CreateTabs(1, false);
for (int i = 0; i < kNumTests; i++) {
open_times[i] = OpenStackView();
close_times[i] = CloseStackView();
LoadNextURL();
}
base::TimeDelta max_open;
base::TimeDelta max_close;
// When calculating the average, only take into account the 'warm' tests.
// i.e. ignore the 'cold' time.
base::TimeDelta open_avg =
CalculateAverage(open_times + 1, kNumTests - 1, NULL, &max_open);
base::TimeDelta close_avg =
CalculateAverage(close_times + 1, kNumTests - 1, NULL, &max_close);
LogPerfTiming("Open cold", open_times[0]);
LogPerfTiming("Open warm avg", open_avg);
LogPerfTiming("Open warm max", max_open);
LogPerfTiming("Close cold", close_times[0]);
LogPerfTiming("Close cold avg", close_avg);
LogPerfTiming("Close cold max", max_close);
}
} // anonymous namespace