blob: 8c202ecb546662b111224e27d3755de33cd011d3 [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* 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.
*/
// Author: jmaessen@google.com (Jan-Willem Maessen)
#include "net/instaweb/rewriter/public/beacon_critical_images_finder.h"
#include "base/logging.h"
#include "net/instaweb/rewriter/critical_images.pb.h"
#include "net/instaweb/rewriter/critical_keys.pb.h"
#include "net/instaweb/rewriter/public/critical_finder_support_util.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/critical_images_finder_test_base.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/timer.h"
using ::testing::Eq;
namespace net_instaweb {
namespace {
const char kRequestUrl[] = "http://www.example.com";
class BeaconCriticalImagesFinderTest : public CriticalImagesFinderTestBase {
public:
virtual CriticalImagesFinder* finder() { return finder_; }
protected:
BeaconCriticalImagesFinderTest() { }
virtual void SetUp() {
CriticalImagesFinderTestBase::SetUp();
const PropertyCache::Cohort* beacon_cohort =
SetupCohort(page_property_cache(), RewriteDriver::kBeaconCohort);
server_context()->set_beacon_cohort(beacon_cohort);
finder_ = new BeaconCriticalImagesFinder(
beacon_cohort, factory()->nonce_generator(), statistics());
server_context()->set_critical_images_finder(finder_);
ResetDriver();
SetDriverRequestHeaders();
// Set up default critical image sets to use for testing.
html_images_.insert("x.jpg");
html_images_.insert("y.png");
html_images_.insert("z.gif");
css_images_.insert("a.jpg");
css_images_.insert("b.png");
css_images_.insert("c.gif");
}
void WriteToPropertyCache() {
rewrite_driver()->property_page()->WriteCohort(
server_context()->beacon_cohort());
}
void WriteBackAndResetDriver() {
WriteToPropertyCache();
ResetDriver();
SetDriverRequestHeaders();
}
GoogleString CriticalImagesString() {
WriteBackAndResetDriver();
const StringSet& html_images =
finder_->GetHtmlCriticalImages(rewrite_driver());
const StringSet& css_images =
finder_->GetCssCriticalImages(rewrite_driver());
GoogleString result = JoinCollection(html_images, ",");
StrAppend(&result, ";");
AppendJoinCollection(&result, css_images, ",");
return result;
}
// Simulate beacon insertion.
void Beacon() {
WriteBackAndResetDriver();
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs);
VerifyBeaconStatus(kBeaconWithNonce);
}
// Same as Beacon(), but advances time by the low frequency beacon interval.
// Useful in cases where a lot of beacons with the same critical image set are
// being sent.
void BeaconLowFrequency() {
WriteBackAndResetDriver();
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs *
kLowFreqBeaconMult);
VerifyBeaconStatus(kBeaconWithNonce);
}
// Verify that no beacon injection occurs.
void VerifyNoBeaconing() {
VerifyBeaconStatus(kDoNotBeacon);
}
// Verify that beacon injection occurs.
void VerifyBeaconing() {
VerifyBeaconStatus(kBeaconWithNonce);
}
// Helper method used for verifying beacon injection status.
void VerifyBeaconStatus(BeaconStatus status) {
bool status_is_beacon_with_nonce = (status == kBeaconWithNonce);
EXPECT_THAT(finder_->ShouldBeacon(rewrite_driver()),
Eq(status_is_beacon_with_nonce));
last_beacon_metadata_ =
finder_->PrepareForBeaconInsertion(rewrite_driver());
EXPECT_EQ(status, last_beacon_metadata_.status);
if (status == kBeaconWithNonce) {
EXPECT_STREQ(ExpectedNonce(), last_beacon_metadata_.nonce);
} else {
EXPECT_TRUE(last_beacon_metadata_.nonce.empty());
}
}
CriticalImages* GetCriticalImages() {
WriteBackAndResetDriver();
EXPECT_TRUE(finder()->IsCriticalImageInfoPresent(rewrite_driver()));
return &rewrite_driver()->critical_images_info()->proto;
}
void CheckDefaultBeaconSupport(int support) {
CheckAXBeaconSupport(support, support, support);
}
void CheckAXBeaconSupport(int a_support, int x_support, int other_support) {
// Inspect support values in the critical images protobuf.
const CriticalImages* critical_images = GetCriticalImages();
const CriticalKeys& html_keys =
critical_images->html_critical_image_support();
const CriticalKeys& css_keys =
critical_images->css_critical_image_support();
ASSERT_EQ(3, html_keys.key_evidence_size());
EXPECT_EQ("x.jpg", html_keys.key_evidence(0).key());
EXPECT_EQ(x_support, html_keys.key_evidence(0).support());
EXPECT_EQ("y.png", html_keys.key_evidence(1).key());
EXPECT_EQ(other_support, html_keys.key_evidence(1).support());
EXPECT_EQ("z.gif", html_keys.key_evidence(2).key());
EXPECT_EQ(other_support, html_keys.key_evidence(2).support());
ASSERT_EQ(3, css_keys.key_evidence_size());
EXPECT_EQ("a.jpg", css_keys.key_evidence(0).key());
EXPECT_EQ(a_support, css_keys.key_evidence(0).support());
EXPECT_EQ("b.png", css_keys.key_evidence(1).key());
EXPECT_EQ(other_support, css_keys.key_evidence(1).support());
EXPECT_EQ("c.gif", css_keys.key_evidence(2).key());
EXPECT_EQ(other_support, css_keys.key_evidence(2).support());
}
bool UpdateCriticalImagesCacheEntry(
const StringSet* html_critical_images_set,
const StringSet* css_critical_images_set) {
// If this fails, you should have called Beacon().
CHECK_EQ(kBeaconWithNonce, last_beacon_metadata_.status);
return UpdateCriticalImagesCacheEntry(
html_critical_images_set, css_critical_images_set,
last_beacon_metadata_.nonce);
}
bool UpdateCriticalImagesCacheEntry(
const StringSet* html_critical_images_set,
const StringSet* css_critical_images_set,
const GoogleString& nonce) {
EXPECT_FALSE(nonce.empty());
return finder_->UpdateCriticalImagesCacheEntry(
html_critical_images_set, css_critical_images_set, NULL,
nonce, server_context()->beacon_cohort(),
rewrite_driver()->property_page(), server_context()->timer());
}
BeaconCriticalImagesFinder* finder_;
BeaconMetadata last_beacon_metadata_;
StringSet html_images_;
StringSet css_images_;
};
TEST_F(BeaconCriticalImagesFinderTest, StoreRestore) {
// Before beacon insertion, nothing in pcache.
CheckCriticalImageFinderStats(0, 0, 0);
CriticalImagesInfo* read_images =
rewrite_driver()->critical_images_info();
EXPECT_TRUE(read_images == NULL);
// Force computation of critical_images_info() via CriticalImagesString()
EXPECT_STREQ(";", CriticalImagesString());
read_images = rewrite_driver()->critical_images_info();
EXPECT_TRUE(read_images != NULL);
// Now beacon and register some critical image results.
Beacon();
CheckCriticalImageFinderStats(0, 0, 2);
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
// Check actual support values, but also verify that images are considered
// critical.
CheckDefaultBeaconSupport(finder_->SupportInterval());
EXPECT_STREQ("x.jpg,y.png,z.gif;a.jpg,b.png,c.gif", CriticalImagesString());
CheckCriticalImageFinderStats(2, 0, 2);
// Now test expiration.
WriteBackAndResetDriver();
AdvanceTimeMs(2 * options()->finder_properties_cache_expiration_time_ms());
read_images = rewrite_driver()->critical_images_info();
EXPECT_TRUE(read_images == NULL);
// Force computation of critical_images_info() via CriticalImagesString()
EXPECT_STREQ(";", CriticalImagesString());
CheckCriticalImageFinderStats(2, 1, 2);
}
// Verify that writing multiple beacon results are stored and aggregated. The
// critical selector set should contain all images seen in the last
// SupportInterval() beacon responses. After SupportInterval() responses,
// beacon results only seen once should no longer be considered critical.
TEST_F(BeaconCriticalImagesFinderTest, StoreMultiple) {
Beacon();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
EXPECT_STREQ("x.jpg,y.png,z.gif;a.jpg,b.png,c.gif", CriticalImagesString());
CheckDefaultBeaconSupport(finder_->SupportInterval());
html_images_.clear();
html_images_.insert("x.jpg");
css_images_.clear();
css_images_.insert("a.jpg");
for (int i = 0; i < finder_->SupportInterval() - 1; ++i) {
BeaconLowFrequency();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
EXPECT_STREQ("x.jpg;a.jpg", CriticalImagesString());
}
// We send two more beacon responses, which should kick a.jpg out of the
// critical css images set as it falls below the 80% support threshold. y.png
// will not accumulate enough support to be considered critical.
css_images_.clear();
html_images_.insert("y.png");
for (int i = 0; i < 2; ++i) {
BeaconLowFrequency();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
}
EXPECT_STREQ("x.jpg;", CriticalImagesString());
}
// Make sure beacon results can arrive out of order (so long as the nonce
// doesn't time out).
TEST_F(BeaconCriticalImagesFinderTest, OutOfOrder) {
// Make sure that the rebeaconing time is less than the time a nonce is valid,
// so that we can test having multiple outstanding nonces.
options()->set_beacon_reinstrument_time_sec(kBeaconTimeoutIntervalMs /
Timer::kSecondMs / 2);
Beacon();
GoogleString initial_nonce(last_beacon_metadata_.nonce);
// A second beacon occurs and the result comes back first.
Beacon();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
EXPECT_STREQ("x.jpg,y.png,z.gif;a.jpg,b.png,c.gif", CriticalImagesString());
CheckDefaultBeaconSupport(finder_->SupportInterval());
// Now the first beacon result comes back out of order. It should still work.
html_images_.clear();
html_images_.insert("x.jpg");
css_images_.clear();
css_images_.insert("a.jpg");
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_images_, &css_images_, initial_nonce));
EXPECT_STREQ("x.jpg;a.jpg", CriticalImagesString());
int supportedTwice = 2 * finder_->SupportInterval() - 1;
CheckAXBeaconSupport(supportedTwice, supportedTwice,
finder_->SupportInterval() - 1);
// A duplicate beacon nonce will be dropped, and support won't change.
EXPECT_FALSE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
EXPECT_STREQ("x.jpg;a.jpg", CriticalImagesString());
CheckAXBeaconSupport(supportedTwice, supportedTwice,
finder_->SupportInterval() - 1);
// As will an entirely bogus nonce (here we use non-base64 characters).
const char kBogusNonce[] = "*&*";
EXPECT_FALSE(UpdateCriticalImagesCacheEntry(
&html_images_, &css_images_, kBogusNonce));
EXPECT_STREQ("x.jpg;a.jpg", CriticalImagesString());
CheckAXBeaconSupport(supportedTwice, supportedTwice,
finder_->SupportInterval() - 1);
}
TEST_F(BeaconCriticalImagesFinderTest, NonceTimeout) {
// Make sure that beacons time out after kBeaconTimeoutIntervalMs.
Beacon();
GoogleString initial_nonce(last_beacon_metadata_.nonce);
// beacon_reinstrument_time_sec() passes (in mock time) before the next call
// completes:
Beacon();
factory()->mock_timer()->AdvanceMs(kBeaconTimeoutIntervalMs);
// This beacon arrives right at its deadline, and is OK.
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&html_images_, &css_images_));
EXPECT_STREQ("x.jpg,y.png,z.gif;a.jpg,b.png,c.gif", CriticalImagesString());
CheckDefaultBeaconSupport(finder_->SupportInterval());
// The first beacon arrives after its deadline, and is dropped.
html_images_.clear();
html_images_.insert("x.jpg");
css_images_.clear();
css_images_.insert("a.jpg");
EXPECT_FALSE(UpdateCriticalImagesCacheEntry(
&html_images_, &css_images_, initial_nonce));
EXPECT_STREQ("x.jpg,y.png,z.gif;a.jpg,b.png,c.gif", CriticalImagesString());
CheckDefaultBeaconSupport(finder_->SupportInterval());
}
TEST_F(BeaconCriticalImagesFinderTest, DontRebeaconBeforeTimeout) {
Beacon();
// Now simulate a beacon insertion attempt without timing out.
WriteBackAndResetDriver();
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs / 2);
BeaconMetadata metadata =
finder_->PrepareForBeaconInsertion(rewrite_driver());
EXPECT_EQ(kDoNotBeacon, metadata.status);
// But we'll re-beacon if some more time passes.
Beacon(); // beacon_reinstrument_time_sec() passes in Beacon() call.
}
TEST_F(BeaconCriticalImagesFinderTest, RebeaconBeforeTimeoutWithHeader) {
Beacon();
// Write a dummy value to the property cache.
WriteToPropertyCache();
// If downstream caching is disabled, any beaconing key configuration and/or
// presence of PS-ShouldBeacon header should be ignored. In such situations,
// unless the reinstrumentation time interval is exceeded, beacon injection
// should not happen.
ResetDriver();
SetDownstreamCacheDirectives("", "", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyNoBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs + 1);
// When the reinstrumentation time interval is exceeded, beacon injection
// should happen as usual.
ResetDriver();
SetDownstreamCacheDirectives("", "", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Beacon injection should not happen when rebeaconing key is not configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", "");
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyNoBeaconing();
// Beacon injection should not happen when the PS-ShouldBeacon header is
// absent and both downstream caching and the associated rebeaconing key
// are configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetDriverRequestHeaders();
VerifyNoBeaconing();
// Beacon injection should not happen when the PS-ShouldBeacon header is
// incorrect.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kWrongBeaconingKey);
VerifyNoBeaconing();
// Beacon injection happens when the PS-ShouldBeacon header is present even
// when the pcache value has not expired and the reinstrumentation time
// interval has not been exceeded.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs + 1);
// Beacon injection should happen after reinstrumentation time interval has
// passed when downstream caching is enabled but rebeaconing key is not
// configured.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", "");
SetShouldBeaconHeader(kConfiguredBeaconingKey);
VerifyBeaconing();
// Advance the timer past the beacon interval.
factory()->mock_timer()->AdvanceMs(
options()->beacon_reinstrument_time_sec() * Timer::kSecondMs + 1);
// Beacon injection should not happen when the PS-ShouldBeacon header is
// incorrect even if the reinstrumentation time interval has been exceeded.
ResetDriver();
SetDownstreamCacheDirectives("", "localhost:80", kConfiguredBeaconingKey);
SetShouldBeaconHeader(kWrongBeaconingKey);
VerifyNoBeaconing();
}
// Verify that sending enough beacons with the same critical image set puts us
// into low frequency beaconing mode.
TEST_F(BeaconCriticalImagesFinderTest, LowFrequencyBeaconing) {
StringSet html_critical_images_set;
html_critical_images_set.insert("x.jpg");
finder_->UpdateCandidateImagesForBeaconing(
html_critical_images_set, rewrite_driver(), false /* beaconing */);
// Send enough beacons to put us into low frequency beaconing mode.
for (int i = 0; i <= kHighFreqBeaconCount; ++i) {
Beacon();
BeaconCriticalImagesFinder::UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL, NULL, last_beacon_metadata_.nonce,
server_context()->beacon_cohort(), rewrite_driver()->property_page(),
factory()->mock_timer());
CriticalKeys* html_critical_images =
GetCriticalImages()->mutable_html_critical_image_support();
EXPECT_THAT(html_critical_images->valid_beacons_received(), Eq(i + 1));
}
// Now we are in low frequency beaconing mode, so advancing by the high
// frequency beaconing amount should not trigger beaconing.
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs);
EXPECT_FALSE(finder_->ShouldBeacon(rewrite_driver()));
// But advancing by the low frequency amount should.
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs * kLowFreqBeaconMult);
Beacon();
factory()->mock_timer()->AdvanceMs(options()->beacon_reinstrument_time_sec() *
Timer::kSecondMs);
VerifyNoBeaconing();
// Now verify that updating the candidate images works correctly. If we are
// beaconing, then the next beacon timestamp does not get updated.
html_critical_images_set.insert("y.jpg");
finder_->UpdateCandidateImagesForBeaconing(
html_critical_images_set, rewrite_driver(), true /* beaconing */);
VerifyNoBeaconing();
// Verify that setting the beaconing flag to false when inserting a new
// candidate key does trigger beaconing on the next request.
html_critical_images_set.insert("z.jpg");
finder_->UpdateCandidateImagesForBeaconing(
html_critical_images_set, rewrite_driver(), false /* beaconing */);
Beacon();
}
} // namespace
} // namespace net_instaweb