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/. */
5 #include "CacheIndex.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"
16 #include "mozilla/Unused.h"
18 namespace mozilla::net::CacheFileUtils
{
20 // This designates the format for the "alt-data" metadata.
21 // When the format changes we need to update the version.
22 static uint32_t const kAltDataVersion
= 1;
23 const char* kAltDataKey
= "alt-data";
28 * A simple recursive descent parser for the mapping key.
30 class KeyParser
: protected Tokenizer
{
32 explicit KeyParser(nsACString
const& aInput
)
35 // Initialize the cache key to a zero length by default
41 OriginAttributes originAttribs
;
44 nsDependentCSubstring cacheKey
;
46 // Keeps the last tag name, used for alphabetical sort checking
49 // Classifier for the 'tag' character valid range.
50 // Explicitly using unsigned char as 127 is -1 when signed and it would only
52 static bool TagChar(const char aChar
) {
53 unsigned char c
= static_cast<unsigned char>(aChar
);
54 return c
>= ' ' && c
<= '\x7f';
58 // Expects to be at the tag name or at the end
64 if (!ReadChar(&TagChar
, &tag
)) {
68 // Check the alphabetical order, hard-fail on disobedience
69 if (!(lastTag
< tag
|| tag
== ':')) {
76 // last possible tag, when present there is the cacheKey following,
77 // not terminated with ',' and no need to unescape.
78 cacheKey
.Rebind(mCursor
, mEnd
- mCursor
);
81 nsAutoCString originSuffix
;
82 if (!ParseValue(&originSuffix
) ||
83 !originAttribs
.PopulateFromSuffix(originSuffix
)) {
89 originAttribs
.SyncAttributesWithPrivateBrowsing(true);
92 // Leaving to be able to read and understand oldformatted entries
98 // Leaving to be able to read and understand oldformatted entries
99 uint32_t deprecatedAppId
= 0;
100 if (!ReadInteger(&deprecatedAppId
)) {
101 return false; // not a valid 32-bit integer
106 if (!ParseValue(&idEnhance
)) {
111 if (!ParseValue()) { // skip any tag values, optional
117 // We expect a comma after every tag
118 if (!CheckChar(',')) {
122 // Recurse to the next tag
126 bool ParseValue(nsACString
* result
= nullptr) {
127 // If at the end, fail since we expect a comma ; value may be empty tho
134 if (!Token::Char(',').Equals(t
)) {
136 result
->Append(t
.Fragment());
141 if (CheckChar(',')) {
142 // Two commas in a row, escaping
149 // We must give the comma back since the upper calls expect it
158 already_AddRefed
<LoadContextInfo
> Parse() {
159 RefPtr
<LoadContextInfo
> info
;
161 info
= GetLoadContextInfo(isAnonymous
, originAttribs
);
164 return info
.forget();
167 void URISpec(nsACString
& result
) { result
.Assign(cacheKey
); }
169 void IdEnhance(nsACString
& result
) { result
.Assign(idEnhance
); }
174 already_AddRefed
<nsILoadContextInfo
> ParseKey(const nsACString
& aKey
,
175 nsACString
* aIdEnhance
,
176 nsACString
* aURISpec
) {
177 KeyParser
parser(aKey
);
178 RefPtr
<LoadContextInfo
> info
= parser
.Parse();
181 if (aIdEnhance
) parser
.IdEnhance(*aIdEnhance
);
182 if (aURISpec
) parser
.URISpec(*aURISpec
);
185 return info
.forget();
188 void AppendKeyPrefix(nsILoadContextInfo
* aInfo
, nsACString
& _retval
) {
190 * This key is used to salt file hashes. When form of the key is changed
191 * cache entries will fail to find on disk.
194 * Keep the attributes list sorted according their ASCII code.
201 OriginAttributes
const* oa
= aInfo
->OriginAttributesPtr();
202 nsAutoCString suffix
;
203 oa
->CreateSuffix(suffix
);
204 if (!suffix
.IsEmpty()) {
205 AppendTagWithValue(_retval
, 'O', suffix
);
208 if (aInfo
->IsAnonymous()) {
209 _retval
.AppendLiteral("a,");
212 if (aInfo
->IsPrivate()) {
213 _retval
.AppendLiteral("p,");
217 void AppendTagWithValue(nsACString
& aTarget
, char const aTag
,
218 const nsACString
& aValue
) {
219 aTarget
.Append(aTag
);
221 // First check the value string to save some memory copying
222 // for cases we don't need to escape at all (most likely).
223 if (!aValue
.IsEmpty()) {
224 if (!aValue
.Contains(',')) {
226 aTarget
.Append(aValue
);
228 nsAutoCString
escapedValue(aValue
);
229 escapedValue
.ReplaceSubstring(","_ns
, ",,"_ns
);
230 aTarget
.Append(escapedValue
);
237 nsresult
KeyMatchesLoadContextInfo(const nsACString
& aKey
,
238 nsILoadContextInfo
* aInfo
, bool* _retval
) {
239 nsCOMPtr
<nsILoadContextInfo
> info
= ParseKey(aKey
);
242 return NS_ERROR_FAILURE
;
245 *_retval
= info
->Equals(aInfo
);
249 ValidityPair::ValidityPair(uint32_t aOffset
, uint32_t aLen
)
250 : mOffset(aOffset
), mLen(aLen
) {}
252 bool ValidityPair::CanBeMerged(const ValidityPair
& aOther
) const {
253 // The pairs can be merged into a single one if the start of one of the pairs
254 // is placed anywhere in the validity interval of other pair or exactly after
256 return IsInOrFollows(aOther
.mOffset
) || aOther
.IsInOrFollows(mOffset
);
259 bool ValidityPair::IsInOrFollows(uint32_t aOffset
) const {
260 return mOffset
<= aOffset
&& mOffset
+ mLen
>= aOffset
;
263 bool ValidityPair::LessThan(const ValidityPair
& aOther
) const {
264 if (mOffset
< aOther
.mOffset
) {
268 if (mOffset
== aOther
.mOffset
&& mLen
< aOther
.mLen
) {
275 void ValidityPair::Merge(const ValidityPair
& aOther
) {
276 MOZ_ASSERT(CanBeMerged(aOther
));
278 uint32_t offset
= std::min(mOffset
, aOther
.mOffset
);
279 uint32_t end
= std::max(mOffset
+ mLen
, aOther
.mOffset
+ aOther
.mLen
);
285 void ValidityMap::Log() const {
286 LOG(("ValidityMap::Log() - number of pairs: %zu", mMap
.Length()));
287 for (uint32_t i
= 0; i
< mMap
.Length(); i
++) {
288 LOG((" (%u, %u)", mMap
[i
].Offset() + 0, mMap
[i
].Len() + 0));
292 uint32_t ValidityMap::Length() const { return mMap
.Length(); }
294 void ValidityMap::AddPair(uint32_t aOffset
, uint32_t aLen
) {
295 ValidityPair
pair(aOffset
, aLen
);
297 if (mMap
.Length() == 0) {
298 mMap
.AppendElement(pair
);
302 // Find out where to place this pair into the map, it can overlap only with
303 // one preceding pair and all subsequent pairs.
305 for (pos
= mMap
.Length(); pos
> 0;) {
308 if (mMap
[pos
].LessThan(pair
)) {
309 // The new pair should be either inserted after pos or merged with it.
310 if (mMap
[pos
].CanBeMerged(pair
)) {
311 // Merge with the preceding pair
312 mMap
[pos
].Merge(pair
);
314 // They don't overlap, element must be placed after pos element
316 if (pos
== mMap
.Length()) {
317 mMap
.AppendElement(pair
);
319 mMap
.InsertElementAt(pos
, pair
);
327 // The new pair should be placed in front of all existing pairs.
328 mMap
.InsertElementAt(0, pair
);
332 // pos now points to merged or inserted pair, check whether it overlaps with
334 while (pos
+ 1 < mMap
.Length()) {
335 if (mMap
[pos
].CanBeMerged(mMap
[pos
+ 1])) {
336 mMap
[pos
].Merge(mMap
[pos
+ 1]);
337 mMap
.RemoveElementAt(pos
+ 1);
344 void ValidityMap::Clear() { mMap
.Clear(); }
346 size_t ValidityMap::SizeOfExcludingThis(
347 mozilla::MallocSizeOf mallocSizeOf
) const {
348 return mMap
.ShallowSizeOfExcludingThis(mallocSizeOf
);
351 ValidityPair
& ValidityMap::operator[](uint32_t aIdx
) {
352 return mMap
.ElementAt(aIdx
);
355 StaticMutex
DetailedCacheHitTelemetry::sLock
;
356 uint32_t DetailedCacheHitTelemetry::sRecordCnt
= 0;
357 MOZ_RUNINIT
DetailedCacheHitTelemetry::HitRate
358 DetailedCacheHitTelemetry::sHRStats
[kNumOfRanges
];
360 DetailedCacheHitTelemetry::HitRate::HitRate() { Reset(); }
362 void DetailedCacheHitTelemetry::HitRate::AddRecord(ERecType aType
) {
370 uint32_t DetailedCacheHitTelemetry::HitRate::GetHitRateBucket() const {
371 if (mHitCnt
+ mMissCnt
== 0) {
374 return (100 * mHitCnt
) / (mHitCnt
+ mMissCnt
);
377 uint32_t DetailedCacheHitTelemetry::HitRate::Count() {
378 return mHitCnt
+ mMissCnt
;
381 void DetailedCacheHitTelemetry::HitRate::Reset() {
387 void DetailedCacheHitTelemetry::AddRecord(ERecType aType
,
388 TimeStamp aLoadStart
) {
389 bool isUpToDate
= false;
390 CacheIndex::IsUpToDate(&isUpToDate
);
392 // Ignore the record when the entry file count might be incorrect
397 nsresult rv
= CacheIndex::GetEntryFileCount(&entryCount
);
402 uint32_t rangeIdx
= entryCount
/ kRangeSize
;
403 if (rangeIdx
>= kNumOfRanges
) { // The last range has no upper limit.
404 rangeIdx
= kNumOfRanges
- 1;
408 nsAutoCString hitMissValue
;
410 hitMissValue
.AppendLiteral("Hit ");
412 hitMissValue
.AppendLiteral("Miss ");
415 uint32_t lowerBound
= rangeIdx
* kRangeSize
;
416 if (rangeIdx
< kNumOfRanges
- 1) {
417 uint32_t upperBound
= (rangeIdx
+ 1) * kRangeSize
- 1;
418 hitMissValue
.AppendInt(lowerBound
);
419 hitMissValue
.AppendLiteral("-");
420 hitMissValue
.AppendInt(upperBound
);
422 // Since no upper limit
423 hitMissValue
.AppendInt(lowerBound
);
424 hitMissValue
.AppendLiteral("+");
428 StaticMutexAutoLock
lock(sLock
);
431 mozilla::glean::network::cache_miss_time
.AccumulateRawDuration(
432 TimeStamp::Now() - aLoadStart
);
434 mozilla::glean::network::cache_hit_time
.AccumulateRawDuration(
435 TimeStamp::Now() - aLoadStart
);
438 mozilla::glean::network::cache_hit_miss_stat_per_cache_size
.Get(hitMissValue
)
442 sHRStats
[rangeIdx
].AddRecord(aType
);
445 if (sRecordCnt
< kTotalSamplesReportLimit
) {
451 for (uint32_t i
= 0; i
< kNumOfRanges
; ++i
) {
452 if (sHRStats
[i
].Count() >= kHitRateSamplesReportLimit
) {
454 nsAutoCString cacheSizeIdx
;
455 cacheSizeIdx
.AppendInt(i
);
456 uint32_t hitRateBucket
= sHRStats
[i
].GetHitRateBucket();
458 mozilla::glean::network::cache_hit_rate_per_cache_size
.Get(cacheSizeIdx
)
459 .AccumulateSingleSample(hitRateBucket
);
466 StaticMutex
CachePerfStats::sLock
;
467 MOZ_RUNINIT
CachePerfStats::PerfData
468 CachePerfStats::sData
[CachePerfStats::LAST
];
469 uint32_t CachePerfStats::sCacheSlowCnt
= 0;
470 uint32_t CachePerfStats::sCacheNotSlowCnt
= 0;
472 CachePerfStats::MMA::MMA(uint32_t aTotalWeight
, bool aFilter
)
473 : mSum(0), mSumSq(0), mCnt(0), mWeight(aTotalWeight
), mFilter(aFilter
) {}
475 void CachePerfStats::MMA::AddValue(uint32_t aValue
) {
477 // Filter high spikes
478 uint32_t avg
= GetAverage();
479 uint32_t stddev
= GetStdDev();
480 uint32_t maxdiff
= avg
+ (3 * stddev
);
481 if (avg
&& aValue
> avg
+ maxdiff
) {
486 if (mCnt
< mWeight
) {
487 // Compute arithmetic average until we have at least mWeight values
488 CheckedInt
<uint64_t> newSumSq
= CheckedInt
<uint64_t>(aValue
) * aValue
;
490 if (!newSumSq
.isValid()) {
491 return; // ignore this value
493 mSumSq
= newSumSq
.value();
497 CheckedInt
<uint64_t> newSumSq
= mSumSq
- mSumSq
/ mCnt
;
498 newSumSq
+= static_cast<uint64_t>(aValue
) * aValue
;
499 if (!newSumSq
.isValid()) {
500 return; // ignore this value
502 mSumSq
= newSumSq
.value();
504 // Compute modified moving average for more values:
505 // newAvg = ((weight - 1) * oldAvg + newValue) / weight
506 mSum
-= GetAverage();
511 uint32_t CachePerfStats::MMA::GetAverage() {
519 uint32_t CachePerfStats::MMA::GetStdDev() {
524 uint32_t avg
= GetAverage();
525 uint64_t avgSq
= static_cast<uint64_t>(avg
) * avg
;
526 uint64_t variance
= mSumSq
/ mCnt
;
527 if (variance
< avgSq
) {
528 // Due to rounding error when using integer data type, it can happen that
529 // average of squares of the values is smaller than square of the average
530 // of the values. In this case fix mSumSq.
532 mSumSq
= variance
* mCnt
;
536 return sqrt(static_cast<double>(variance
));
539 CachePerfStats::PerfData::PerfData()
540 : mFilteredAvg(50, true), mShortAvg(3, false) {}
542 void CachePerfStats::PerfData::AddValue(uint32_t aValue
, bool aShortOnly
) {
544 mFilteredAvg
.AddValue(aValue
);
546 mShortAvg
.AddValue(aValue
);
549 uint32_t CachePerfStats::PerfData::GetAverage(bool aFiltered
) {
550 return aFiltered
? mFilteredAvg
.GetAverage() : mShortAvg
.GetAverage();
553 uint32_t CachePerfStats::PerfData::GetStdDev(bool aFiltered
) {
554 return aFiltered
? mFilteredAvg
.GetStdDev() : mShortAvg
.GetStdDev();
558 void CachePerfStats::AddValue(EDataType aType
, uint32_t aValue
,
560 StaticMutexAutoLock
lock(sLock
);
561 sData
[aType
].AddValue(aValue
, aShortOnly
);
565 uint32_t CachePerfStats::GetAverage(EDataType aType
, bool aFiltered
) {
566 StaticMutexAutoLock
lock(sLock
);
567 return sData
[aType
].GetAverage(aFiltered
);
571 uint32_t CachePerfStats::GetStdDev(EDataType aType
, bool aFiltered
) {
572 StaticMutexAutoLock
lock(sLock
);
573 return sData
[aType
].GetStdDev(aFiltered
);
577 bool CachePerfStats::IsCacheSlow() {
578 StaticMutexAutoLock
lock(sLock
);
580 // Compare mShortAvg with mFilteredAvg to find out whether cache is getting
581 // slower. Use only data about single IO operations because ENTRY_OPEN can be
582 // affected by more factors than a slow disk.
583 for (uint32_t i
= 0; i
< ENTRY_OPEN
; ++i
) {
585 // Skip this data type. IsCacheSlow is used for determining cache slowness
586 // when opening entries. Writes have low priority and it's normal that
587 // they are delayed a lot, but this doesn't necessarily affect opening
592 uint32_t avgLong
= sData
[i
].GetAverage(true);
594 // We have no perf data yet, skip this data type.
597 uint32_t avgShort
= sData
[i
].GetAverage(false);
598 uint32_t stddevLong
= sData
[i
].GetStdDev(true);
599 uint32_t maxdiff
= avgLong
+ (3 * stddevLong
);
601 if (avgShort
> avgLong
+ maxdiff
) {
603 ("CachePerfStats::IsCacheSlow() - result is slow based on perf "
604 "type %u [avgShort=%u, avgLong=%u, stddevLong=%u]",
605 i
, avgShort
, avgLong
, stddevLong
));
616 void CachePerfStats::GetSlowStats(uint32_t* aSlow
, uint32_t* aNotSlow
) {
617 StaticMutexAutoLock
lock(sLock
);
618 *aSlow
= sCacheSlowCnt
;
619 *aNotSlow
= sCacheNotSlowCnt
;
622 void FreeBuffer(void* aBuf
) {
623 #ifndef NS_FREE_PERMANENT_DATA
624 if (CacheObserver::ShuttingDown()) {
632 nsresult
ParseAlternativeDataInfo(const char* aInfo
, int64_t* _offset
,
634 // The format is: "1;12345,javascript/binary"
635 // <version>;<offset>,<type>
636 mozilla::Tokenizer
p(aInfo
, nullptr, "/");
637 uint32_t altDataVersion
= 0;
638 int64_t altDataOffset
= -1;
640 // The metadata format has a wrong version number.
641 if (!p
.ReadInteger(&altDataVersion
) || altDataVersion
!= kAltDataVersion
) {
643 ("ParseAlternativeDataInfo() - altDataVersion=%u, "
644 "expectedVersion=%u",
645 altDataVersion
, kAltDataVersion
));
646 return NS_ERROR_NOT_AVAILABLE
;
649 if (!p
.CheckChar(';') || !p
.ReadInteger(&altDataOffset
) ||
651 return NS_ERROR_NOT_AVAILABLE
;
654 // The requested alt-data representation is not available
655 if (altDataOffset
< 0) {
656 return NS_ERROR_NOT_AVAILABLE
;
660 *_offset
= altDataOffset
;
664 mozilla::Unused
<< p
.ReadUntil(Tokenizer::Token::EndOfFile(), *_type
);
670 void BuildAlternativeDataInfo(const char* aInfo
, int64_t aOffset
,
671 nsACString
& _retval
) {
673 _retval
.AppendInt(kAltDataVersion
);
675 _retval
.AppendInt(aOffset
);
677 _retval
.Append(aInfo
);
680 } // namespace mozilla::net::CacheFileUtils