CacheFileUtils.cpp (18109B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "CacheIndex.h" 6 #include "CacheLog.h" 7 #include "CacheFileUtils.h" 8 #include "CacheObserver.h" 9 #include "LoadContextInfo.h" 10 #include "mozilla/glean/NetwerkProtocolHttpMetrics.h" 11 #include "mozilla/Tokenizer.h" 12 #include "mozilla/Telemetry.h" 13 #include "nsCOMPtr.h" 14 #include "nsString.h" 15 #include <algorithm> 16 17 namespace mozilla::net::CacheFileUtils { 18 19 // This designates the format for the "alt-data" metadata. 20 // When the format changes we need to update the version. 21 static uint32_t const kAltDataVersion = 1; 22 const char* kAltDataKey = "alt-data"; 23 24 namespace { 25 26 /** 27 * A simple recursive descent parser for the mapping key. 28 */ 29 class KeyParser : protected Tokenizer { 30 public: 31 explicit KeyParser(nsACString const& aInput) 32 : Tokenizer(aInput), 33 isAnonymous(false) 34 // Initialize the cache key to a zero length by default 35 , 36 lastTag(0) {} 37 38 private: 39 // Results 40 OriginAttributes originAttribs; 41 bool isAnonymous; 42 nsCString idEnhance; 43 nsDependentCSubstring cacheKey; 44 45 // Keeps the last tag name, used for alphabetical sort checking 46 char lastTag; 47 48 // Classifier for the 'tag' character valid range. 49 // Explicitly using unsigned char as 127 is -1 when signed and it would only 50 // produce a warning. 51 static bool TagChar(const char aChar) { 52 unsigned char c = static_cast<unsigned char>(aChar); 53 return c >= ' ' && c <= '\x7f'; 54 } 55 56 bool ParseTags() { 57 // Expects to be at the tag name or at the end 58 if (CheckEOF()) { 59 return true; 60 } 61 62 char tag; 63 if (!ReadChar(&TagChar, &tag)) { 64 return false; 65 } 66 67 // Check the alphabetical order, hard-fail on disobedience 68 if (!(lastTag < tag || tag == ':')) { 69 return false; 70 } 71 lastTag = tag; 72 73 switch (tag) { 74 case ':': 75 // last possible tag, when present there is the cacheKey following, 76 // not terminated with ',' and no need to unescape. 77 cacheKey.Rebind(mCursor, mEnd - mCursor); 78 return true; 79 case 'O': { 80 nsAutoCString originSuffix; 81 if (!ParseValue(&originSuffix) || 82 !originAttribs.PopulateFromSuffix(originSuffix)) { 83 return false; 84 } 85 break; 86 } 87 case 'p': 88 originAttribs.SyncAttributesWithPrivateBrowsing(true); 89 break; 90 case 'b': 91 // Leaving to be able to read and understand oldformatted entries 92 break; 93 case 'a': 94 isAnonymous = true; 95 break; 96 case 'i': { 97 // Leaving to be able to read and understand oldformatted entries 98 uint32_t deprecatedAppId = 0; 99 if (!ReadInteger(&deprecatedAppId)) { 100 return false; // not a valid 32-bit integer 101 } 102 break; 103 } 104 case '~': 105 if (!ParseValue(&idEnhance)) { 106 return false; 107 } 108 break; 109 default: 110 if (!ParseValue()) { // skip any tag values, optional 111 return false; 112 } 113 break; 114 } 115 116 // We expect a comma after every tag 117 if (!CheckChar(',')) { 118 return false; 119 } 120 121 // Recurse to the next tag 122 return ParseTags(); 123 } 124 125 bool ParseValue(nsACString* result = nullptr) { 126 // If at the end, fail since we expect a comma ; value may be empty tho 127 if (CheckEOF()) { 128 return false; 129 } 130 131 Token t; 132 while (Next(t)) { 133 if (!Token::Char(',').Equals(t)) { 134 if (result) { 135 result->Append(t.Fragment()); 136 } 137 continue; 138 } 139 140 if (CheckChar(',')) { 141 // Two commas in a row, escaping 142 if (result) { 143 result->Append(','); 144 } 145 continue; 146 } 147 148 // We must give the comma back since the upper calls expect it 149 Rollback(); 150 return true; 151 } 152 153 return false; 154 } 155 156 public: 157 already_AddRefed<LoadContextInfo> Parse() { 158 RefPtr<LoadContextInfo> info; 159 if (ParseTags()) { 160 info = GetLoadContextInfo(isAnonymous, originAttribs); 161 } 162 163 return info.forget(); 164 } 165 166 void URISpec(nsACString& result) { result.Assign(cacheKey); } 167 168 void IdEnhance(nsACString& result) { result.Assign(idEnhance); } 169 }; 170 171 } // namespace 172 173 already_AddRefed<nsILoadContextInfo> ParseKey(const nsACString& aKey, 174 nsACString* aIdEnhance, 175 nsACString* aURISpec) { 176 KeyParser parser(aKey); 177 RefPtr<LoadContextInfo> info = parser.Parse(); 178 179 if (info) { 180 if (aIdEnhance) parser.IdEnhance(*aIdEnhance); 181 if (aURISpec) parser.URISpec(*aURISpec); 182 } 183 184 return info.forget(); 185 } 186 187 void AppendKeyPrefix(nsILoadContextInfo* aInfo, nsACString& _retval) { 188 /** 189 * This key is used to salt file hashes. When form of the key is changed 190 * cache entries will fail to find on disk. 191 * 192 * IMPORTANT NOTE: 193 * Keep the attributes list sorted according their ASCII code. 194 */ 195 196 if (!aInfo) { 197 return; 198 } 199 200 OriginAttributes const* oa = aInfo->OriginAttributesPtr(); 201 nsAutoCString suffix; 202 oa->CreateSuffix(suffix); 203 if (!suffix.IsEmpty()) { 204 AppendTagWithValue(_retval, 'O', suffix); 205 } 206 207 if (aInfo->IsAnonymous()) { 208 _retval.AppendLiteral("a,"); 209 } 210 211 if (aInfo->IsPrivate()) { 212 _retval.AppendLiteral("p,"); 213 } 214 } 215 216 void AppendTagWithValue(nsACString& aTarget, char const aTag, 217 const nsACString& aValue) { 218 aTarget.Append(aTag); 219 220 // First check the value string to save some memory copying 221 // for cases we don't need to escape at all (most likely). 222 if (!aValue.IsEmpty()) { 223 if (!aValue.Contains(',')) { 224 // No need to escape 225 aTarget.Append(aValue); 226 } else { 227 nsAutoCString escapedValue(aValue); 228 escapedValue.ReplaceSubstring(","_ns, ",,"_ns); 229 aTarget.Append(escapedValue); 230 } 231 } 232 233 aTarget.Append(','); 234 } 235 236 nsresult KeyMatchesLoadContextInfo(const nsACString& aKey, 237 nsILoadContextInfo* aInfo, bool* _retval) { 238 nsCOMPtr<nsILoadContextInfo> info = ParseKey(aKey); 239 240 if (!info) { 241 return NS_ERROR_FAILURE; 242 } 243 244 *_retval = info->Equals(aInfo); 245 return NS_OK; 246 } 247 248 ValidityPair::ValidityPair(uint32_t aOffset, uint32_t aLen) 249 : mOffset(aOffset), mLen(aLen) {} 250 251 bool ValidityPair::CanBeMerged(const ValidityPair& aOther) const { 252 // The pairs can be merged into a single one if the start of one of the pairs 253 // is placed anywhere in the validity interval of other pair or exactly after 254 // its end. 255 return IsInOrFollows(aOther.mOffset) || aOther.IsInOrFollows(mOffset); 256 } 257 258 bool ValidityPair::IsInOrFollows(uint32_t aOffset) const { 259 return mOffset <= aOffset && mOffset + mLen >= aOffset; 260 } 261 262 bool ValidityPair::LessThan(const ValidityPair& aOther) const { 263 if (mOffset < aOther.mOffset) { 264 return true; 265 } 266 267 if (mOffset == aOther.mOffset && mLen < aOther.mLen) { 268 return true; 269 } 270 271 return false; 272 } 273 274 void ValidityPair::Merge(const ValidityPair& aOther) { 275 MOZ_ASSERT(CanBeMerged(aOther)); 276 277 uint32_t offset = std::min(mOffset, aOther.mOffset); 278 uint32_t end = std::max(mOffset + mLen, aOther.mOffset + aOther.mLen); 279 280 mOffset = offset; 281 mLen = end - offset; 282 } 283 284 void ValidityMap::Log() const { 285 LOG(("ValidityMap::Log() - number of pairs: %zu", mMap.Length())); 286 for (uint32_t i = 0; i < mMap.Length(); i++) { 287 LOG((" (%u, %u)", mMap[i].Offset() + 0, mMap[i].Len() + 0)); 288 } 289 } 290 291 uint32_t ValidityMap::Length() const { return mMap.Length(); } 292 293 void ValidityMap::AddPair(uint32_t aOffset, uint32_t aLen) { 294 ValidityPair pair(aOffset, aLen); 295 296 if (mMap.Length() == 0) { 297 mMap.AppendElement(pair); 298 return; 299 } 300 301 // Find out where to place this pair into the map, it can overlap only with 302 // one preceding pair and all subsequent pairs. 303 uint32_t pos = 0; 304 for (pos = mMap.Length(); pos > 0;) { 305 --pos; 306 307 if (mMap[pos].LessThan(pair)) { 308 // The new pair should be either inserted after pos or merged with it. 309 if (mMap[pos].CanBeMerged(pair)) { 310 // Merge with the preceding pair 311 mMap[pos].Merge(pair); 312 } else { 313 // They don't overlap, element must be placed after pos element 314 ++pos; 315 if (pos == mMap.Length()) { 316 mMap.AppendElement(pair); 317 } else { 318 mMap.InsertElementAt(pos, pair); 319 } 320 } 321 322 break; 323 } 324 325 if (pos == 0) { 326 // The new pair should be placed in front of all existing pairs. 327 mMap.InsertElementAt(0, pair); 328 } 329 } 330 331 // pos now points to merged or inserted pair, check whether it overlaps with 332 // subsequent pairs. 333 while (pos + 1 < mMap.Length()) { 334 if (mMap[pos].CanBeMerged(mMap[pos + 1])) { 335 mMap[pos].Merge(mMap[pos + 1]); 336 mMap.RemoveElementAt(pos + 1); 337 } else { 338 break; 339 } 340 } 341 } 342 343 void ValidityMap::Clear() { mMap.Clear(); } 344 345 size_t ValidityMap::SizeOfExcludingThis( 346 mozilla::MallocSizeOf mallocSizeOf) const { 347 return mMap.ShallowSizeOfExcludingThis(mallocSizeOf); 348 } 349 350 ValidityPair& ValidityMap::operator[](uint32_t aIdx) { 351 return mMap.ElementAt(aIdx); 352 } 353 354 StaticMutex DetailedCacheHitTelemetry::sLock; 355 uint32_t DetailedCacheHitTelemetry::sRecordCnt = 0; 356 MOZ_RUNINIT DetailedCacheHitTelemetry::HitRate 357 DetailedCacheHitTelemetry::sHRStats[kNumOfRanges]; 358 359 DetailedCacheHitTelemetry::HitRate::HitRate() { Reset(); } 360 361 void DetailedCacheHitTelemetry::HitRate::AddRecord(ERecType aType) { 362 if (aType == HIT) { 363 ++mHitCnt; 364 } else { 365 ++mMissCnt; 366 } 367 } 368 369 uint32_t DetailedCacheHitTelemetry::HitRate::GetHitRateBucket() const { 370 if (mHitCnt + mMissCnt == 0) { 371 return 0; 372 } 373 return (100 * mHitCnt) / (mHitCnt + mMissCnt); 374 } 375 376 uint32_t DetailedCacheHitTelemetry::HitRate::Count() { 377 return mHitCnt + mMissCnt; 378 } 379 380 void DetailedCacheHitTelemetry::HitRate::Reset() { 381 mHitCnt = 0; 382 mMissCnt = 0; 383 } 384 385 // static 386 void DetailedCacheHitTelemetry::AddRecord(ERecType aType, 387 TimeStamp aLoadStart) { 388 bool isUpToDate = false; 389 CacheIndex::IsUpToDate(&isUpToDate); 390 if (!isUpToDate) { 391 // Ignore the record when the entry file count might be incorrect 392 return; 393 } 394 395 uint32_t entryCount; 396 nsresult rv = CacheIndex::GetEntryFileCount(&entryCount); 397 if (NS_FAILED(rv)) { 398 return; 399 } 400 401 uint32_t rangeIdx = entryCount / kRangeSize; 402 if (rangeIdx >= kNumOfRanges) { // The last range has no upper limit. 403 rangeIdx = kNumOfRanges - 1; 404 } 405 406 #ifndef ANDROID 407 nsAutoCString hitMissValue; 408 if (aType == HIT) { 409 hitMissValue.AppendLiteral("Hit "); 410 } else { 411 hitMissValue.AppendLiteral("Miss "); 412 } 413 414 uint32_t lowerBound = rangeIdx * kRangeSize; 415 if (rangeIdx < kNumOfRanges - 1) { 416 uint32_t upperBound = (rangeIdx + 1) * kRangeSize - 1; 417 hitMissValue.AppendInt(lowerBound); 418 hitMissValue.AppendLiteral("-"); 419 hitMissValue.AppendInt(upperBound); 420 } else { 421 // Since no upper limit 422 hitMissValue.AppendInt(lowerBound); 423 hitMissValue.AppendLiteral("+"); 424 } 425 #endif 426 427 StaticMutexAutoLock lock(sLock); 428 429 if (aType == MISS) { 430 mozilla::glean::network::cache_miss_time.AccumulateRawDuration( 431 TimeStamp::Now() - aLoadStart); 432 } else { 433 mozilla::glean::network::cache_hit_time.AccumulateRawDuration( 434 TimeStamp::Now() - aLoadStart); 435 } 436 #ifndef ANDROID 437 mozilla::glean::network::cache_hit_miss_stat_per_cache_size.Get(hitMissValue) 438 .Add(1); 439 #endif 440 441 sHRStats[rangeIdx].AddRecord(aType); 442 ++sRecordCnt; 443 444 if (sRecordCnt < kTotalSamplesReportLimit) { 445 return; 446 } 447 448 sRecordCnt = 0; 449 450 for (uint32_t i = 0; i < kNumOfRanges; ++i) { 451 if (sHRStats[i].Count() >= kHitRateSamplesReportLimit) { 452 #ifndef ANDROID 453 nsAutoCString cacheSizeIdx; 454 cacheSizeIdx.AppendInt(i); 455 uint32_t hitRateBucket = sHRStats[i].GetHitRateBucket(); 456 457 mozilla::glean::network::cache_hit_rate_per_cache_size.Get(cacheSizeIdx) 458 .AccumulateSingleSample(hitRateBucket); 459 #endif 460 sHRStats[i].Reset(); 461 } 462 } 463 } 464 465 StaticMutex CachePerfStats::sLock; 466 MOZ_RUNINIT CachePerfStats::PerfData 467 CachePerfStats::sData[CachePerfStats::LAST]; 468 uint32_t CachePerfStats::sCacheSlowCnt = 0; 469 uint32_t CachePerfStats::sCacheNotSlowCnt = 0; 470 471 CachePerfStats::MMA::MMA(uint32_t aTotalWeight, bool aFilter) 472 : mSum(0), mSumSq(0), mCnt(0), mWeight(aTotalWeight), mFilter(aFilter) {} 473 474 void CachePerfStats::MMA::AddValue(uint32_t aValue) { 475 if (mFilter) { 476 // Filter high spikes 477 uint32_t avg = GetAverage(); 478 uint32_t stddev = GetStdDev(); 479 uint32_t maxdiff = avg + (3 * stddev); 480 if (avg && aValue > avg + maxdiff) { 481 return; 482 } 483 } 484 485 if (mCnt < mWeight) { 486 // Compute arithmetic average until we have at least mWeight values 487 CheckedInt<uint64_t> newSumSq = CheckedInt<uint64_t>(aValue) * aValue; 488 newSumSq += mSumSq; 489 if (!newSumSq.isValid()) { 490 return; // ignore this value 491 } 492 mSumSq = newSumSq.value(); 493 mSum += aValue; 494 ++mCnt; 495 } else { 496 CheckedInt<uint64_t> newSumSq = mSumSq - mSumSq / mCnt; 497 newSumSq += static_cast<uint64_t>(aValue) * aValue; 498 if (!newSumSq.isValid()) { 499 return; // ignore this value 500 } 501 mSumSq = newSumSq.value(); 502 503 // Compute modified moving average for more values: 504 // newAvg = ((weight - 1) * oldAvg + newValue) / weight 505 mSum -= GetAverage(); 506 mSum += aValue; 507 } 508 } 509 510 uint32_t CachePerfStats::MMA::GetAverage() { 511 if (mCnt == 0) { 512 return 0; 513 } 514 515 return mSum / mCnt; 516 } 517 518 uint32_t CachePerfStats::MMA::GetStdDev() { 519 if (mCnt == 0) { 520 return 0; 521 } 522 523 uint32_t avg = GetAverage(); 524 uint64_t avgSq = static_cast<uint64_t>(avg) * avg; 525 uint64_t variance = mSumSq / mCnt; 526 if (variance < avgSq) { 527 // Due to rounding error when using integer data type, it can happen that 528 // average of squares of the values is smaller than square of the average 529 // of the values. In this case fix mSumSq. 530 variance = avgSq; 531 mSumSq = variance * mCnt; 532 } 533 534 variance -= avgSq; 535 return sqrt(static_cast<double>(variance)); 536 } 537 538 CachePerfStats::PerfData::PerfData() 539 : mFilteredAvg(50, true), mShortAvg(3, false) {} 540 541 void CachePerfStats::PerfData::AddValue(uint32_t aValue, bool aShortOnly) { 542 if (!aShortOnly) { 543 mFilteredAvg.AddValue(aValue); 544 } 545 mShortAvg.AddValue(aValue); 546 } 547 548 uint32_t CachePerfStats::PerfData::GetAverage(bool aFiltered) { 549 return aFiltered ? mFilteredAvg.GetAverage() : mShortAvg.GetAverage(); 550 } 551 552 uint32_t CachePerfStats::PerfData::GetStdDev(bool aFiltered) { 553 return aFiltered ? mFilteredAvg.GetStdDev() : mShortAvg.GetStdDev(); 554 } 555 556 // static 557 void CachePerfStats::AddValue(EDataType aType, uint32_t aValue, 558 bool aShortOnly) { 559 StaticMutexAutoLock lock(sLock); 560 sData[aType].AddValue(aValue, aShortOnly); 561 } 562 563 // static 564 uint32_t CachePerfStats::GetAverage(EDataType aType, bool aFiltered) { 565 StaticMutexAutoLock lock(sLock); 566 return sData[aType].GetAverage(aFiltered); 567 } 568 569 // static 570 uint32_t CachePerfStats::GetStdDev(EDataType aType, bool aFiltered) { 571 StaticMutexAutoLock lock(sLock); 572 return sData[aType].GetStdDev(aFiltered); 573 } 574 575 // static 576 bool CachePerfStats::IsCacheSlow() { 577 StaticMutexAutoLock lock(sLock); 578 579 // Compare mShortAvg with mFilteredAvg to find out whether cache is getting 580 // slower. Use only data about single IO operations because ENTRY_OPEN can be 581 // affected by more factors than a slow disk. 582 for (uint32_t i = 0; i < ENTRY_OPEN; ++i) { 583 if (i == IO_WRITE) { 584 // Skip this data type. IsCacheSlow is used for determining cache slowness 585 // when opening entries. Writes have low priority and it's normal that 586 // they are delayed a lot, but this doesn't necessarily affect opening 587 // cache entries. 588 continue; 589 } 590 591 uint32_t avgLong = sData[i].GetAverage(true); 592 if (avgLong == 0) { 593 // We have no perf data yet, skip this data type. 594 continue; 595 } 596 uint32_t avgShort = sData[i].GetAverage(false); 597 uint32_t stddevLong = sData[i].GetStdDev(true); 598 uint32_t maxdiff = avgLong + (3 * stddevLong); 599 600 if (avgShort > avgLong + maxdiff) { 601 LOG( 602 ("CachePerfStats::IsCacheSlow() - result is slow based on perf " 603 "type %u [avgShort=%u, avgLong=%u, stddevLong=%u]", 604 i, avgShort, avgLong, stddevLong)); 605 ++sCacheSlowCnt; 606 return true; 607 } 608 } 609 610 ++sCacheNotSlowCnt; 611 return false; 612 } 613 614 // static 615 void CachePerfStats::GetSlowStats(uint32_t* aSlow, uint32_t* aNotSlow) { 616 StaticMutexAutoLock lock(sLock); 617 *aSlow = sCacheSlowCnt; 618 *aNotSlow = sCacheNotSlowCnt; 619 } 620 621 void FreeBuffer(void* aBuf) { 622 #ifndef NS_FREE_PERMANENT_DATA 623 if (CacheObserver::ShuttingDown()) { 624 return; 625 } 626 #endif 627 628 free(aBuf); 629 } 630 631 nsresult ParseAlternativeDataInfo(const char* aInfo, int64_t* _offset, 632 nsACString* _type) { 633 // The format is: "1;12345,javascript/binary" 634 // <version>;<offset>,<type> 635 mozilla::Tokenizer p(aInfo, nullptr, "/"); 636 uint32_t altDataVersion = 0; 637 int64_t altDataOffset = -1; 638 639 // The metadata format has a wrong version number. 640 if (!p.ReadInteger(&altDataVersion) || altDataVersion != kAltDataVersion) { 641 LOG( 642 ("ParseAlternativeDataInfo() - altDataVersion=%u, " 643 "expectedVersion=%u", 644 altDataVersion, kAltDataVersion)); 645 return NS_ERROR_NOT_AVAILABLE; 646 } 647 648 if (!p.CheckChar(';') || !p.ReadInteger(&altDataOffset) || 649 !p.CheckChar(',')) { 650 return NS_ERROR_NOT_AVAILABLE; 651 } 652 653 // The requested alt-data representation is not available 654 if (altDataOffset < 0) { 655 return NS_ERROR_NOT_AVAILABLE; 656 } 657 658 if (_offset) { 659 *_offset = altDataOffset; 660 } 661 662 if (_type) { 663 (void)p.ReadUntil(Tokenizer::Token::EndOfFile(), *_type); 664 } 665 666 return NS_OK; 667 } 668 669 void BuildAlternativeDataInfo(const char* aInfo, int64_t aOffset, 670 nsACString& _retval) { 671 _retval.Truncate(); 672 _retval.AppendInt(kAltDataVersion); 673 _retval.Append(';'); 674 _retval.AppendInt(aOffset); 675 _retval.Append(','); 676 _retval.Append(aInfo); 677 } 678 679 } // namespace mozilla::net::CacheFileUtils