blob: a9623765cda36075ef20894cf824a5f67a975df6 [file] [log] [blame]
/*
* Copyright 2012 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: pulkitg@google.com (Pulkit Goyal)
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/rewriter/critical_images.pb.h"
#include "net/instaweb/rewriter/critical_keys.pb.h"
#include "net/instaweb/rewriter/public/critical_images_finder_test_base.h"
#include "net/instaweb/rewriter/public/property_cache_util.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/rendered_image.pb.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/proto_util.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/http/google_url.h"
namespace net_instaweb {
namespace {
// Mock class for testing a critical image finder like the beacon finder that
// stores a history of previous critical image sets.
class HistoryTestCriticalImagesFinder : public TestCriticalImagesFinder {
public:
HistoryTestCriticalImagesFinder(const PropertyCache::Cohort* cohort,
Statistics* stats)
: TestCriticalImagesFinder(cohort, stats) {}
virtual int PercentSeenForCritical() const {
return 80;
}
virtual int SupportInterval() const {
return 10;
}
};
const char kCriticalImagesCohort[] = "critical_images";
} // namespace
class CriticalImagesFinderTest : public CriticalImagesFinderTestBase {
public:
virtual CriticalImagesFinder* finder() { return finder_.get(); }
protected:
virtual void SetUp() {
CriticalImagesFinderTestBase::SetUp();
SetupCohort(page_property_cache(), kCriticalImagesCohort);
finder_.reset(new TestCriticalImagesFinder(
page_property_cache()->GetCohort(kCriticalImagesCohort), statistics()));
ResetDriver();
}
private:
friend class CriticalImagesHistoryFinderTest;
scoped_ptr<TestCriticalImagesFinder> finder_;
};
class CriticalImagesHistoryFinderTest : public CriticalImagesFinderTest {
protected:
virtual void SetUp() {
CriticalImagesFinderTestBase::SetUp();
SetupCohort(page_property_cache(), kCriticalImagesCohort);
finder_.reset(new HistoryTestCriticalImagesFinder(
page_property_cache()->GetCohort(kCriticalImagesCohort), statistics()));
ResetDriver();
}
};
TEST_F(CriticalImagesFinderTest, UpdateCriticalImagesCacheEntrySuccess) {
// Include an actual value in the RPC result to induce a cache write.
StringSet html_critical_images_set;
html_critical_images_set.insert("imageA.jpeg");
StringSet css_critical_images_set;
css_critical_images_set.insert("imageB.jpeg");
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, &css_critical_images_set));
EXPECT_TRUE(GetCriticalImagesUpdatedValue()->has_value());
// Verify the contents of the support protobuf, and ensure we're no longer
// generating legacy data.
ArrayInputStream input(GetCriticalImagesUpdatedValue()->value().data(),
GetCriticalImagesUpdatedValue()->value().size());
CriticalImages parsed_proto;
parsed_proto.ParseFromZeroCopyStream(&input);
ASSERT_TRUE(parsed_proto.has_html_critical_image_support());
const CriticalKeys& html_support = parsed_proto.html_critical_image_support();
EXPECT_EQ(1, html_support.key_evidence_size());
ASSERT_TRUE(parsed_proto.has_css_critical_image_support());
const CriticalKeys& css_support = parsed_proto.css_critical_image_support();
EXPECT_EQ(1, css_support.key_evidence_size());
}
TEST_F(CriticalImagesFinderTest,
UpdateCriticalImagesCacheEntrySuccessEmptySet) {
// Include an actual value in the RPC result to induce a cache write.
StringSet html_critical_images_set;
StringSet css_critical_images_set;
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, &css_critical_images_set));
EXPECT_TRUE(GetCriticalImagesUpdatedValue()->has_value());
EXPECT_TRUE(GetCriticalImagesUpdatedValue()->has_value());
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
}
TEST_F(CriticalImagesFinderTest, UpdateCriticalImagesCacheEntrySetNULL) {
EXPECT_FALSE(UpdateCriticalImagesCacheEntry(NULL, NULL));
EXPECT_FALSE(GetCriticalImagesUpdatedValue()->has_value());
}
TEST_F(CriticalImagesFinderTest,
UpdateCriticalImagesCacheEntryPropertyPageMissing) {
// No cache insert if PropertyPage is not set in RewriteDriver.
rewrite_driver()->set_property_page(NULL);
// Include an actual value in the RPC result to induce a cache write. We
// expect no writes, but not from a lack of results!
StringSet html_critical_images_set;
StringSet css_critical_images_set;
EXPECT_FALSE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, &css_critical_images_set));
EXPECT_EQ(NULL, GetCriticalImagesUpdatedValue());
}
TEST_F(CriticalImagesFinderTest, GetCriticalImagesTest) {
// First it will insert the value in cache, then it retrieves critical images.
// Include an actual value in the RPC result to induce a cache write.
StringSet html_critical_images_set;
html_critical_images_set.insert("imageA.jpeg");
html_critical_images_set.insert("imageB.jpeg");
StringSet css_critical_images_set;
css_critical_images_set.insert("imageD.jpeg");
// Calling IsHtmlCriticalImage should update the CriticalImagesInfo in
// RewriteDriver.
EXPECT_FALSE(IsHtmlCriticalImage("imageA.jpg"));
// We should get 1 miss for the critical images value.
CheckCriticalImageFinderStats(0, 0, 1);
// Here and below, -1 results mean "no critical image data reported".
EXPECT_EQ(-1, logging_info()->num_html_critical_images());
EXPECT_EQ(-1, logging_info()->num_css_critical_images());
ClearStats();
// Calling IsHtmlCriticalImage again should not update the stats, because the
// CriticalImagesInfo has already been updated.
EXPECT_FALSE(IsHtmlCriticalImage("imageA.jpg"));
CheckCriticalImageFinderStats(0, 0, 0);
// ClearStats() creates a new request context and hence a new log record. So
// the critical image counts are not set.
EXPECT_EQ(-1, logging_info()->num_html_critical_images());
EXPECT_EQ(-1, logging_info()->num_css_critical_images());
ClearStats();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, &css_critical_images_set));
// Write the updated value to the pcache.
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
EXPECT_TRUE(GetCriticalImagesUpdatedValue()->has_value());
// critical_images_info() is NULL because there is no previous call to
// GetCriticalImages()
ResetDriver();
EXPECT_TRUE(rewrite_driver()->critical_images_info() == NULL);
EXPECT_TRUE(IsHtmlCriticalImage("imageA.jpeg"));
CheckCriticalImageFinderStats(1, 0, 0);
EXPECT_EQ(2, logging_info()->num_html_critical_images());
EXPECT_EQ(1, logging_info()->num_css_critical_images());
ClearStats();
// GetCriticalImages() updates critical_images set in RewriteDriver().
EXPECT_TRUE(rewrite_driver()->critical_images_info() != NULL);
// EXPECT_EQ(2, GetCriticalImages(rewrite_driver()).size());
EXPECT_TRUE(IsHtmlCriticalImage("imageA.jpeg"));
EXPECT_TRUE(IsHtmlCriticalImage("imageB.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imageC.jpeg"));
// EXPECT_EQ(1, css_critical_images->size());
EXPECT_TRUE(IsCssCriticalImage("imageD.jpeg"));
EXPECT_FALSE(IsCssCriticalImage("imageA.jpeg"));
// Reset the driver, read the page and call UpdateCriticalImagesSetInDriver by
// calling IsHtmlCriticalImage.
// We read it from cache.
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imageA.jpeg"));
CheckCriticalImageFinderStats(1, 0, 0);
EXPECT_EQ(2, logging_info()->num_html_critical_images());
EXPECT_EQ(1, logging_info()->num_css_critical_images());
ClearStats();
// Advance to 90% of expiry. We get a hit from cache and must_compute is true.
AdvanceTimeMs(0.9 * options()->finder_properties_cache_expiration_time_ms());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imageA.jpeg"));
CheckCriticalImageFinderStats(1, 0, 0);
EXPECT_EQ(2, logging_info()->num_html_critical_images());
EXPECT_EQ(1, logging_info()->num_css_critical_images());
ClearStats();
ResetDriver();
// Advance past expiry, so that the pages expire; now no images are critical.
AdvanceTimeMs(2 * options()->finder_properties_cache_expiration_time_ms());
EXPECT_TRUE(rewrite_driver()->critical_images_info() == NULL);
EXPECT_FALSE(IsHtmlCriticalImage("imageA.jpeg"));
EXPECT_TRUE(rewrite_driver()->critical_images_info() != NULL);
CheckCriticalImageFinderStats(0, 1, 0);
// Here -1 results mean "no valid critical image data" due to expiry.
EXPECT_EQ(-1, logging_info()->num_html_critical_images());
EXPECT_EQ(-1, logging_info()->num_css_critical_images());
}
TEST_F(CriticalImagesHistoryFinderTest, GetCriticalImagesTest) {
// Verify that storing multiple critical images, like we do with the beacon
// critical image finder, works correctly.
// Write images to property cache, ensuring that they are critical images
StringSet html_critical_images_set;
html_critical_images_set.insert("imgA.jpeg");
html_critical_images_set.insert("imgB.jpeg");
StringSet css_critical_images_set;
css_critical_images_set.insert("imgD.jpeg");
for (int i = 0; i < finder()->SupportInterval() * 3; ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, &css_critical_images_set));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_TRUE(IsHtmlCriticalImage("imgB.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
EXPECT_FALSE(IsCssCriticalImage("imgA.jpeg"));
}
// Now, write just imgA twice. Since our limit is set to 80%, B should still
// be critical afterwards.
html_critical_images_set.clear();
html_critical_images_set.insert("imgA.jpeg");
for (int i = 0; i < 2; ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_TRUE(IsHtmlCriticalImage("imgB.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
}
// Continue writing imgA, but now imgB should be below our threshold.
for (int i = 0; i < finder()->SupportInterval(); ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgB.jpeg"));
// We didn't write CSS critical images, so imgD should still be critical.
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
}
// Write imgC twice. imgA should still be critical, and C should not.
html_critical_images_set.clear();
html_critical_images_set.insert("imgC.jpeg");
for (int i = 0; i < 2; ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgB.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgC.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
}
// Continue writing imgC; it won't have enough support to make it critical,
// and A should no longer be critical. That's because the maximum possible
// support value will have saturated, so we need a fair amount of support
// before we reach the saturated value. Basically we're iterating until:
// sum{k<-1..n} ((s(s-1))/s)^k >= r sum{k<-1..infinity} ((s(s-1)/s)^k
// And in this case, where s=10 and r=80%, k happens to be 14 (2 iterations
// above and 12 iterations here). To make things more fun, the above
// calculations are done approximately using integer arithmetic, which makes
// the limit much easier to compute.
for (int i = 0; i < 12; ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_FALSE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgB.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgC.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
}
// And finally, write imgC, making sure it is critical.
for (int i = 0; i < finder()->SupportInterval(); ++i) {
ResetDriver();
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(
&html_critical_images_set, NULL));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_FALSE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_FALSE(IsHtmlCriticalImage("imgB.jpeg"));
EXPECT_TRUE(IsHtmlCriticalImage("imgC.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgD.jpeg"));
}
}
TEST_F(CriticalImagesFinderTest, NoCriticalImages) {
// Make sure we deal gracefully when there are no critical images in a beacon
// result.
StringSet critical;
EXPECT_TRUE(critical.empty());
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&critical, &critical));
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_FALSE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_FALSE(IsCssCriticalImage("imgA.jpeg"));
EXPECT_TRUE(finder()->GetHtmlCriticalImages(rewrite_driver()).empty());
EXPECT_TRUE(finder()->GetCssCriticalImages(rewrite_driver()).empty());
// Now register critical images and make sure we can leave the empty state.
critical.insert("imgA.jpeg");
for (int i = 0; i < finder()->SupportInterval(); ++i) {
EXPECT_TRUE(UpdateCriticalImagesCacheEntry(&critical, &critical));
}
rewrite_driver()->property_page()->WriteCohort(finder()->cohort());
ResetDriver();
EXPECT_TRUE(IsHtmlCriticalImage("imgA.jpeg"));
EXPECT_TRUE(IsCssCriticalImage("imgA.jpeg"));
}
TEST_F(CriticalImagesFinderTest, TestRenderedImageExtractionFromPropertyCache) {
RenderedImages rendered_images;
RenderedImages_Image* images = rendered_images.add_image();
GoogleString url_str = "http://example.com/imageA.jpeg";
images->set_src(url_str);
images->set_rendered_width(40);
images->set_rendered_height(54);
PropertyPage* page = rewrite_driver()->property_page();
UpdateInPropertyCache(rendered_images, finder()->cohort(),
finder()->kRenderedImageDimensionsProperty,
false /* don't write cohort */, page);
// Check if Finder extracts properly.
scoped_ptr<RenderedImages> extracted_rendered_images(
finder()->ExtractRenderedImageDimensionsFromCache(rewrite_driver()));
EXPECT_EQ(1, extracted_rendered_images->image_size());
EXPECT_STREQ(url_str, extracted_rendered_images->image(0).src());
EXPECT_EQ(40, extracted_rendered_images->image(0).rendered_width());
EXPECT_EQ(54, extracted_rendered_images->image(0).rendered_height());
options()->EnableFilter(RewriteOptions::kResizeToRenderedImageDimensions);
std::pair<int32, int32> dimensions;
GoogleUrl gurl(url_str);
EXPECT_TRUE(finder()->GetRenderedImageDimensions(rewrite_driver(), gurl,
&dimensions));
EXPECT_EQ(std::make_pair(40, 54), dimensions);
}
} // namespace net_instaweb