blob: 9f56cbc1feb1b67e11cf229204d7b2ec778dc184 [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: jmarantz@google.com (Joshua Marantz)
#include "pagespeed/opt/http/property_cache.h"
#include <algorithm>
#include <utility>
#include "base/logging.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/callback.h"
#include "pagespeed/kernel/base/stl_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/cache/cache_stats.h"
#include "pagespeed/opt/http/abstract_property_store_get_callback.h"
#include "pagespeed/opt/http/property_cache.pb.h"
#include "pagespeed/opt/http/property_store.h"
#include "pagespeed/opt/logging/log_record.h"
namespace net_instaweb {
namespace {
const int kDefaultMutationsPer1000WritesThreshold = 300;
// http://stackoverflow.com/questions/109023/
// best-algorithm-to-count-the-number-of-set-bits-in-a-32-bit-integer
//
// Note that gcc has __builtin_popcount(i) and we could conceivably
// exploit that. Also note that g++ does not seem to support that on
// MacOS. In the future we could consider switching to that
// implementation if this ever was measured as a performance
// bottleneck.
inline uint32 NumberOfSetBits32(uint32 i) {
uint32 num_set = i - ((i >> 1) & 0x55555555);
num_set = (num_set & 0x33333333) + ((num_set >> 2) & 0x33333333);
return (((num_set + (num_set >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}
inline int NumberOfSetBits64(uint64 i) {
return (NumberOfSetBits32(static_cast<uint32>(i)) +
NumberOfSetBits32(static_cast<uint32>(i >> 32)));
}
} // namespace
PropertyCache::PropertyCache(PropertyStore* property_store,
Timer* timer,
Statistics* stats,
ThreadSystem* threads)
: property_store_(property_store),
timer_(timer),
stats_(stats),
thread_system_(threads),
mutations_per_1000_writes_threshold_(
kDefaultMutationsPer1000WritesThreshold),
enabled_(true) {
}
PropertyCache::~PropertyCache() {
STLDeleteValues(&cohorts_);
}
void PropertyPage::AddValueFromProtobuf(
const PropertyCache::Cohort* cohort,
const PropertyValueProtobuf& pcache_value) {
ScopedMutex lock(mutex_.get());
CohortDataMap::iterator cohort_itr = cohort_data_map_.find(cohort);
CHECK(cohort_itr != cohort_data_map_.end());
PropertyMapStruct* pmap_struct = cohort_itr->second;
PropertyMap* pmap = &pmap_struct->pmap;
PropertyValue* property = (*pmap)[pcache_value.name()];
if (property == NULL) {
property = new PropertyValue;
(*pmap)[pcache_value.name()] = property;
log_record()->AddFoundPropertyToCohortInfo(
page_type_, cohort->name(), pcache_value.name());
}
pmap_struct->has_value = true;
property->InitFromProtobuf(pcache_value);
}
void PropertyPage::SetupCohorts(
const PropertyCache::CohortVector& cohort_list) {
for (int j = 0, n = cohort_list.size(); j < n; ++j) {
const PropertyCache::Cohort* cohort = cohort_list[j];
PropertyMapStruct* pmap_struct = new PropertyMapStruct(log_record());
cohort_data_map_[cohort] = pmap_struct;
}
}
bool PropertyPage::EncodePropertyCacheValues(
const PropertyCache::Cohort* cohort, PropertyCacheValues* values) {
ScopedMutex lock(mutex_.get());
CohortDataMap::const_iterator p = cohort_data_map_.find(cohort);
if (p == cohort_data_map_.end()) {
return false;
}
bool ret = false;
PropertyMap* pmap = &p->second->pmap;
for (PropertyMap::iterator p = pmap->begin(), e = pmap->end();
p != e; ++p) {
PropertyValue* property = p->second;
PropertyValueProtobuf* pcache_value = property->protobuf();
if (pcache_value->name().empty()) {
pcache_value->set_name(p->first);
}
// Why might the value be empty? If a cache lookup is performed, misses,
// and UpdateValue() is never called. In this case, we can skip the
// write.
if (!pcache_value->body().empty()) {
*values->add_value() = *pcache_value;
ret = true;
}
}
return ret;
}
StringPiece PropertyPage::PageTypeSuffix(PageType type) {
switch (type) {
case kPropertyCachePage:
case kPropertyCacheFallbackPage:
return "";
case kPropertyCachePerOriginPage:
return "@PerSite";
}
LOG(DFATAL) << "Weird property page type:" << type;
return "";
}
bool PropertyPage::HasPropertyValueDeleted(
const PropertyCache::Cohort* cohort) {
bool ret = false;
{
ScopedMutex lock(mutex_.get());
CohortDataMap::const_iterator p = cohort_data_map_.find(cohort);
if (p != cohort_data_map_.end()) {
ret = p->second->has_deleted_property;
}
}
return ret;
}
PropertyValue::PropertyValue()
: proto_(new PropertyValueProtobuf),
changed_(true),
valid_(false),
was_read_(false) {
}
PropertyValue::~PropertyValue() {
}
void PropertyValue::InitFromProtobuf(const PropertyValueProtobuf& value) {
proto_.reset(new PropertyValueProtobuf(value));
changed_ = false;
valid_ = true;
was_read_ = true;
}
void PropertyValue::SetValue(const StringPiece& value, int64 now_ms) {
if (!valid_ || (value != proto_->body())) {
valid_ = true;
changed_ = true;
value.CopyToString(proto_->mutable_body());
}
proto_->set_update_mask((proto_->update_mask() << 1) | changed_);
proto_->set_num_writes(proto_->num_writes() + 1);
proto_->set_write_timestamp_ms(now_ms);
}
StringPiece PropertyValue::value() const {
return StringPiece(proto_->body());
}
int64 PropertyValue::write_timestamp_ms() const {
return proto_->write_timestamp_ms();
}
// TODO(hujie): Remove Read after all original Read calls are changed to
// ReadWithCohorts.
void PropertyCache::Read(PropertyPage* page) const {
ReadWithCohorts(cohort_list_, page);
}
void PropertyCache::ReadWithCohorts(const CohortVector& cohort_list,
PropertyPage* page) const {
if (!enabled_ || cohort_list.empty()) {
page->Abort();
return;
}
page->Read(cohort_list);
}
void PropertyPage::Abort() {
CallDone(false);
}
void PropertyPage::Read(const PropertyCache::CohortVector& cohort_list) {
DCHECK(!cohort_list.empty());
DCHECK(property_store_callback_ == NULL);
SetupCohorts(cohort_list);
property_cache_->property_store()->Get(
url_,
options_signature_hash_,
cache_key_suffix_,
cohort_list,
this,
NewCallback(this, &PropertyPage::CallDone),
&property_store_callback_);
}
bool PropertyValue::IsStable(int mutations_per_1000_threshold) const {
// We allocate a 64-bit mask to record whether recent calls to Write
// actually changed the data. So although we keep a total number of
// Writes that is not clamped to 64, we need to clamp between 1-64 so
// we can use this as a divisor to determine stability.
int num_writes = std::max(static_cast<int64>(1),
std::min(static_cast<int64>(64),
proto_->num_writes()));
int num_changes = NumberOfSetBits64(proto_->update_mask());
int changes_per_1000_writes = (1000 * num_changes) / num_writes;
return (changes_per_1000_writes < mutations_per_1000_threshold);
}
bool PropertyValue::IsRecentlyConstant(int num_writes_unchanged) const {
int num_pcache_writes = proto_->num_writes();
if (num_writes_unchanged > 64) {
// We track at most 64 writes in update_mask.
return false;
}
// If we have not seen num_writes_unchanged writes then just check whether all
// the writes were for the same value.
if (num_writes_unchanged > num_pcache_writes) {
num_writes_unchanged = num_pcache_writes;
}
uint64 update_mask = proto_->update_mask();
// Check if the index of least set bit for update_mask is >=
// num_writes_unchanged. OR all writes are for the same value.
return !IsIndexOfLeastSetBitSmaller(update_mask, num_writes_unchanged) ||
(update_mask == 0);
}
bool PropertyValue::IsIndexOfLeastSetBitSmaller(uint64 value, int index) {
uint64 check_mask = static_cast<uint64>(1) << std::max(index - 1, 0);
return ((value & ~(value - 1)) < check_mask);
}
bool PropertyCache::IsExpired(const PropertyValue* property_value,
int64 ttl_ms) const {
DCHECK(property_value->has_value());
int64 expiration_time_ms = property_value->write_timestamp_ms() + ttl_ms;
return timer_->NowMs() > expiration_time_ms;
}
const PropertyCache::Cohort* PropertyCache::AddCohort(
const StringPiece& cohort_name) {
PropertyCache::Cohort* cohort = new PropertyCache::Cohort(cohort_name);
std::pair<CohortMap::iterator, bool> insertions = cohorts_.insert(
make_pair(cohort->name(), static_cast<Cohort*>(NULL)));
CHECK(insertions.second) << cohort->name() << " is added twice.";
insertions.first->second = cohort;
cohort_list_.push_back(insertions.first->second);
return cohort;
}
const PropertyCache::Cohort* PropertyCache::GetCohort(
const StringPiece& cohort_name) const {
GoogleString cohort_string;
cohort_name.CopyToString(&cohort_string);
CohortMap::const_iterator p = cohorts_.find(cohort_string);
if (p == cohorts_.end()) {
return NULL;
}
const Cohort* cohort = p->second;
return cohort;
}
GoogleString PropertyCache::GetStatsPrefix(const GoogleString& cohort_name) {
return StrCat("pcache-cohorts-", cohort_name);
}
void PropertyCache::InitCohortStats(const GoogleString& cohort,
Statistics* statistics) {
CacheStats::InitStats(GetStatsPrefix(cohort), statistics);
}
AbstractPropertyPage::~AbstractPropertyPage() {
}
PropertyPage::PropertyPage(
PageType page_type,
StringPiece url,
StringPiece options_signature_hash,
StringPiece cache_key_suffix,
const RequestContextPtr& request_context,
AbstractMutex* mutex,
PropertyCache* property_cache)
: mutex_(mutex),
url_(url.as_string()),
options_signature_hash_(options_signature_hash.as_string()),
cache_key_suffix_(
StrCat(cache_key_suffix, PageTypeSuffix(page_type))),
request_context_(request_context),
was_read_(false),
property_cache_(property_cache),
property_store_callback_(NULL),
page_type_(page_type) {
}
PropertyPage::~PropertyPage() {
if (property_store_callback_ != NULL) {
property_store_callback_->DeleteWhenDone();
}
while (!cohort_data_map_.empty()) {
CohortDataMap::iterator p = cohort_data_map_.begin();
PropertyMapStruct* pmap_struct = p->second;
PropertyMap* pmap = &p->second->pmap;
cohort_data_map_.erase(p);
// TODO(jmarantz): Not currently Using STLDeleteValues because
// ~PropertyValue is private, the syntax for friending template
// functions eludes me.
for (PropertyMap::iterator p = pmap->begin(), e = pmap->end(); p != e;
++p) {
PropertyValue* value = p->second;
delete value;
}
delete pmap_struct;
}
}
PropertyValue* PropertyPage::GetProperty(
const PropertyCache::Cohort* cohort,
const StringPiece& property_name) {
ScopedMutex lock(mutex_.get());
DCHECK(was_read_);
DCHECK(cohort != NULL);
PropertyValue* property = NULL;
GoogleString property_name_str(property_name.data(), property_name.size());
CohortDataMap::const_iterator cohort_itr = cohort_data_map_.find(cohort);
CHECK(cohort_itr != cohort_data_map_.end());
PropertyMapStruct* pmap_struct = cohort_itr->second;
PropertyMap* pmap = &pmap_struct->pmap;
property = (*pmap)[property_name_str];
log_record()->AddRetrievedPropertyToCohortInfo(
page_type_, cohort->name(), property_name.as_string());
if (property == NULL) {
property = new PropertyValue;
(*pmap)[property_name_str] = property;
property->set_was_read(was_read_);
}
return property;
}
void PropertyPage::UpdateValue(
const PropertyCache::Cohort* cohort, const StringPiece& property_name,
const StringPiece& value) {
if (cohort == NULL) {
// TODO(pulkitg): Change LOG(WARNING) to LOG(DFATAL).
LOG(WARNING) << "Cohort is NULL in PropertyPage::UpdateValue()";
return;
}
PropertyValue* property = GetProperty(cohort, property_name);
int64 now_ms = property_cache_->timer()->NowMs();
// TODO(jmarantz): the policy of not having old timestamps override
// new timestamps can cause us to discard some writes when
// system-time jumps backwards, which can happen for various
// reasons. I think will need to revisit this policy as we learn how
// to use the property cache & get the dynamics we want.
if (property->write_timestamp_ms() <= now_ms) {
property->SetValue(value, now_ms);
}
}
void PropertyPage::WriteCohort(const PropertyCache::Cohort* cohort) {
if (cohort == NULL) {
// TODO(pulkitg): Change LOG(WARNING) to LOG(DFATAL).
LOG(WARNING) << "Cohort is NULL in PropertyPage::WriteCohort()";
return;
}
if (property_cache_->enabled()) {
PropertyCacheValues values;
if (EncodePropertyCacheValues(cohort, &values) ||
HasPropertyValueDeleted(cohort)) {
property_cache_->property_store()->Put(
url_,
options_signature_hash_,
cache_key_suffix_,
cohort,
&values,
NULL);
}
}
}
CacheInterface::KeyState PropertyPage::GetCacheState(
const PropertyCache::Cohort* cohort) {
ScopedMutex lock(mutex_.get());
DCHECK(was_read_);
DCHECK(cohort != NULL);
CohortDataMap::iterator cohort_itr = cohort_data_map_.find(cohort);
CHECK(cohort_itr != cohort_data_map_.end());
PropertyMapStruct* pmap_struct = cohort_itr->second;
return pmap_struct->cache_state;
}
void PropertyPage::SetCacheState(
const PropertyCache::Cohort* cohort,
CacheInterface::KeyState x) {
ScopedMutex lock(mutex_.get());
DCHECK(cohort != NULL);
CohortDataMap::iterator cohort_itr = cohort_data_map_.find(cohort);
CHECK(cohort_itr != cohort_data_map_.end());
PropertyMapStruct* pmap_struct = cohort_itr->second;
pmap_struct->cache_state = x;
}
void PropertyPage::DeleteProperty(
const PropertyCache::Cohort* cohort, const StringPiece& property_name) {
DCHECK(was_read_);
DCHECK(cohort != NULL);
ScopedMutex lock(mutex_.get());
CohortDataMap::iterator cohort_itr = cohort_data_map_.find(cohort);
if (cohort_itr == cohort_data_map_.end()) {
return;
}
PropertyMapStruct* pmap_struct = cohort_itr->second;
PropertyMap* pmap = &pmap_struct->pmap;
PropertyMap::iterator pmap_itr = pmap->find(property_name.as_string());
if (pmap_itr == pmap->end()) {
return;
}
PropertyValue* property = pmap_itr->second;
pmap->erase(pmap_itr);
pmap_struct->has_deleted_property = true;
delete property;
}
bool PropertyPage::IsCohortPresent(const PropertyCache::Cohort* cohort) {
ScopedMutex lock(mutex_.get());
DCHECK(cohort != NULL);
CohortDataMap::iterator cohort_itr = cohort_data_map_.find(cohort);
CHECK(cohort_itr != cohort_data_map_.end());
PropertyMapStruct* pmap_struct = cohort_itr->second;
return pmap_struct->has_value;
}
void PropertyPage::FastFinishLookup() {
if (property_store_callback_ != NULL) {
property_store_callback_->FastFinishLookup();
}
}
} // namespace net_instaweb