b94d6124c5
RewriteDriver::SetUserAgent in favor of the setting the user-agent in request headers, which the RewriteDriver reads.
459 lines
19 KiB
C++
459 lines
19 KiB
C++
/*
|
|
* 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
|