| /* |
| * 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 |