1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifndef ProfileBufferEntry_h 8 #define ProfileBufferEntry_h 9 10 #include "ProfileJSONWriter.h" 11 12 #include "gtest/MozGtestFriend.h" 13 #include "js/ProfilingCategory.h" 14 #include "js/ProfilingFrameIterator.h" 15 #include "mozilla/HashFunctions.h" 16 #include "mozilla/HashTable.h" 17 #include "mozilla/Maybe.h" 18 #include "mozilla/UniquePtr.h" 19 #include "mozilla/Variant.h" 20 #include "mozilla/Vector.h" 21 #include "nsString.h" 22 23 class ProfilerCodeAddressService; 24 25 // NOTE! If you add entries, you need to verify if they need to be added to the 26 // switch statement in DuplicateLastSample! 27 // This will evaluate the MACRO with (KIND, TYPE, SIZE) 28 #define FOR_EACH_PROFILE_BUFFER_ENTRY_KIND(MACRO) \ 29 MACRO(CategoryPair, int, sizeof(int)) \ 30 MACRO(CollectionStart, double, sizeof(double)) \ 31 MACRO(CollectionEnd, double, sizeof(double)) \ 32 MACRO(Label, const char*, sizeof(const char*)) \ 33 MACRO(FrameFlags, uint64_t, sizeof(uint64_t)) \ 34 MACRO(DynamicStringFragment, char*, ProfileBufferEntry::kNumChars) \ 35 MACRO(JitReturnAddr, void*, sizeof(void*)) \ 36 MACRO(InnerWindowID, uint64_t, sizeof(uint64_t)) \ 37 MACRO(LineNumber, int, sizeof(int)) \ 38 MACRO(ColumnNumber, int, sizeof(int)) \ 39 MACRO(NativeLeafAddr, void*, sizeof(void*)) \ 40 MACRO(Pause, double, sizeof(double)) \ 41 MACRO(Resume, double, sizeof(double)) \ 42 MACRO(ThreadId, int, sizeof(int)) \ 43 MACRO(Time, double, sizeof(double)) \ 44 MACRO(TimeBeforeCompactStack, double, sizeof(double)) \ 45 MACRO(CounterId, void*, sizeof(void*)) \ 46 MACRO(CounterKey, uint64_t, sizeof(uint64_t)) \ 47 MACRO(Number, uint64_t, sizeof(uint64_t)) \ 48 MACRO(Count, int64_t, sizeof(int64_t)) \ 49 MACRO(ProfilerOverheadTime, double, sizeof(double)) \ 50 MACRO(ProfilerOverheadDuration, double, sizeof(double)) 51 52 class ProfileBufferEntry { 53 public: 54 // The `Kind` is a single byte identifying the type of data that is actually 55 // stored in a `ProfileBufferEntry`, as per the list in 56 // `FOR_EACH_PROFILE_BUFFER_ENTRY_KIND`. 57 // 58 // This byte is also used to identify entries in ProfileChunkedBuffer blocks, 59 // for both "legacy" entries that do contain a `ProfileBufferEntry`, and for 60 // new types of entries that may carry more data of different types. 61 // TODO: Eventually each type of "legacy" entry should be replaced with newer, 62 // more efficient kinds of entries (e.g., stack frames could be stored in one 63 // bigger entry, instead of multiple `ProfileBufferEntry`s); then we could 64 // discard `ProfileBufferEntry` and move this enum to a more appropriate spot. 65 using KindUnderlyingType = uint8_t; 66 enum class Kind : KindUnderlyingType { 67 INVALID = 0, 68 #define KIND(KIND, TYPE, SIZE) KIND, 69 FOR_EACH_PROFILE_BUFFER_ENTRY_KIND(KIND) 70 #undef KIND 71 72 // Any value under `LEGACY_LIMIT` represents a `ProfileBufferEntry`. 73 LEGACY_LIMIT, 74 75 // Any value starting here does *not* represent a `ProfileBufferEntry` and 76 // requires separate decoding and handling. 77 78 // Marker data, including payload. 79 MarkerData = LEGACY_LIMIT, 80 81 // Optional between TimeBeforeCompactStack and CompactStack. 82 UnresponsiveDurationMs, 83 84 // Collection of legacy stack entries, must follow a ThreadId and 85 // TimeBeforeCompactStack (which are not included in the CompactStack; 86 // TimeBeforeCompactStack is equivalent to Time, but indicates that a 87 // CompactStack follows shortly afterwards). 88 CompactStack, 89 90 MODERN_LIMIT 91 }; 92 93 ProfileBufferEntry(); 94 95 // This is equal to sizeof(double), which is the largest non-char variant in 96 // |u|. 97 static const size_t kNumChars = 8; 98 99 private: 100 // aString must be a static string. 101 ProfileBufferEntry(Kind aKind, const char* aString); 102 ProfileBufferEntry(Kind aKind, char aChars[kNumChars]); 103 ProfileBufferEntry(Kind aKind, void* aPtr); 104 ProfileBufferEntry(Kind aKind, double aDouble); 105 ProfileBufferEntry(Kind aKind, int64_t aInt64); 106 ProfileBufferEntry(Kind aKind, uint64_t aUint64); 107 ProfileBufferEntry(Kind aKind, int aInt); 108 109 public: 110 #define CTOR(KIND, TYPE, SIZE) \ 111 static ProfileBufferEntry KIND(TYPE aVal) { \ 112 return ProfileBufferEntry(Kind::KIND, aVal); \ 113 } FOR_EACH_PROFILE_BUFFER_ENTRY_KIND(CTOR)114 FOR_EACH_PROFILE_BUFFER_ENTRY_KIND(CTOR) 115 #undef CTOR 116 117 Kind GetKind() const { return mKind; } 118 119 #define IS_KIND(KIND, TYPE, SIZE) \ 120 bool Is##KIND() const { return mKind == Kind::KIND; } 121 FOR_EACH_PROFILE_BUFFER_ENTRY_KIND(IS_KIND) 122 #undef IS_KIND 123 124 private: 125 FRIEND_TEST(ThreadProfile, InsertOneEntry); 126 FRIEND_TEST(ThreadProfile, InsertOneEntryWithTinyBuffer); 127 FRIEND_TEST(ThreadProfile, InsertEntriesNoWrap); 128 FRIEND_TEST(ThreadProfile, InsertEntriesWrap); 129 FRIEND_TEST(ThreadProfile, MemoryMeasure); 130 friend class ProfileBuffer; 131 132 Kind mKind; 133 uint8_t mStorage[kNumChars]; 134 135 const char* GetString() const; 136 void* GetPtr() const; 137 double GetDouble() const; 138 int GetInt() const; 139 int64_t GetInt64() const; 140 uint64_t GetUint64() const; 141 void CopyCharsInto(char (&aOutArray)[kNumChars]) const; 142 }; 143 144 // Packed layout: 1 byte for the tag + 8 bytes for the value. 145 static_assert(sizeof(ProfileBufferEntry) == 9, "bad ProfileBufferEntry size"); 146 147 class UniqueJSONStrings { 148 public: 149 UniqueJSONStrings(); 150 explicit UniqueJSONStrings(const UniqueJSONStrings& aOther); 151 SpliceStringTableElements(SpliceableJSONWriter & aWriter)152 void SpliceStringTableElements(SpliceableJSONWriter& aWriter) { 153 aWriter.TakeAndSplice(mStringTableWriter.WriteFunc()); 154 } 155 WriteProperty(mozilla::JSONWriter & aWriter,const char * aName,const char * aStr)156 void WriteProperty(mozilla::JSONWriter& aWriter, const char* aName, 157 const char* aStr) { 158 aWriter.IntProperty(aName, GetOrAddIndex(aStr)); 159 } 160 WriteElement(mozilla::JSONWriter & aWriter,const char * aStr)161 void WriteElement(mozilla::JSONWriter& aWriter, const char* aStr) { 162 aWriter.IntElement(GetOrAddIndex(aStr)); 163 } 164 165 uint32_t GetOrAddIndex(const char* aStr); 166 167 private: 168 SpliceableChunkedJSONWriter mStringTableWriter; 169 mozilla::HashMap<mozilla::HashNumber, uint32_t> mStringHashToIndexMap; 170 }; 171 172 // Contains all the information about JIT frames that is needed to stream stack 173 // frames for JitReturnAddr entries in the profiler buffer. 174 // Every return address (void*) is mapped to one or more JITFrameKeys, and 175 // every JITFrameKey is mapped to a JSON string for that frame. 176 // mRangeStart and mRangeEnd describe the range in the buffer for which this 177 // mapping is valid. Only JitReturnAddr entries within that buffer range can be 178 // processed using this JITFrameInfoForBufferRange object. 179 struct JITFrameInfoForBufferRange final { 180 JITFrameInfoForBufferRange Clone() const; 181 182 uint64_t mRangeStart; 183 uint64_t mRangeEnd; // mRangeEnd marks the first invalid index. 184 185 struct JITFrameKey { 186 bool operator==(const JITFrameKey& aOther) const { 187 return mCanonicalAddress == aOther.mCanonicalAddress && 188 mDepth == aOther.mDepth; 189 } 190 bool operator!=(const JITFrameKey& aOther) const { 191 return !(*this == aOther); 192 } 193 194 void* mCanonicalAddress; 195 uint32_t mDepth; 196 }; 197 struct JITFrameKeyHasher { 198 using Lookup = JITFrameKey; 199 hashfinal::JITFrameKeyHasher200 static mozilla::HashNumber hash(const JITFrameKey& aLookup) { 201 mozilla::HashNumber hash = 0; 202 hash = mozilla::AddToHash(hash, aLookup.mCanonicalAddress); 203 hash = mozilla::AddToHash(hash, aLookup.mDepth); 204 return hash; 205 } 206 matchfinal::JITFrameKeyHasher207 static bool match(const JITFrameKey& aKey, const JITFrameKey& aLookup) { 208 return aKey == aLookup; 209 } 210 rekeyfinal::JITFrameKeyHasher211 static void rekey(JITFrameKey& aKey, const JITFrameKey& aNewKey) { 212 aKey = aNewKey; 213 } 214 }; 215 216 using JITAddressToJITFramesMap = 217 mozilla::HashMap<void*, mozilla::Vector<JITFrameKey>>; 218 JITAddressToJITFramesMap mJITAddressToJITFramesMap; 219 using JITFrameToFrameJSONMap = 220 mozilla::HashMap<JITFrameKey, nsCString, JITFrameKeyHasher>; 221 JITFrameToFrameJSONMap mJITFrameToFrameJSONMap; 222 }; 223 224 // Contains JITFrameInfoForBufferRange objects for multiple profiler buffer 225 // ranges. 226 struct JITFrameInfo final { JITFrameInfofinal227 JITFrameInfo() : mUniqueStrings(mozilla::MakeUnique<UniqueJSONStrings>()) {} 228 229 MOZ_IMPLICIT JITFrameInfo(const JITFrameInfo& aOther); 230 231 // Creates a new JITFrameInfoForBufferRange object in mRanges by looking up 232 // information about the provided JIT return addresses using aCx. 233 // Addresses are provided like this: 234 // The caller of AddInfoForRange supplies a function in aJITAddressProvider. 235 // This function will be called once, synchronously, with an 236 // aJITAddressConsumer argument, which is a function that needs to be called 237 // for every address. That function can be called multiple times for the same 238 // address. 239 void AddInfoForRange( 240 uint64_t aRangeStart, uint64_t aRangeEnd, JSContext* aCx, 241 const std::function<void(const std::function<void(void*)>&)>& 242 aJITAddressProvider); 243 244 // Returns whether the information stored in this object is still relevant 245 // for any entries in the buffer. HasExpiredfinal246 bool HasExpired(uint64_t aCurrentBufferRangeStart) const { 247 if (mRanges.empty()) { 248 // No information means no relevant information. Allow this object to be 249 // discarded. 250 return true; 251 } 252 return mRanges.back().mRangeEnd <= aCurrentBufferRangeStart; 253 } 254 255 // The array of ranges of JIT frame information, sorted by buffer position. 256 // Ranges are non-overlapping. 257 // The JSON of the cached frames can contain string indexes, which refer 258 // to strings in mUniqueStrings. 259 mozilla::Vector<JITFrameInfoForBufferRange> mRanges; 260 261 // The string table which contains strings used in the frame JSON that's 262 // cached in mRanges. 263 mozilla::UniquePtr<UniqueJSONStrings> mUniqueStrings; 264 }; 265 266 class UniqueStacks { 267 public: 268 struct FrameKey { FrameKeyFrameKey269 explicit FrameKey(const char* aLocation) 270 : mData(NormalFrameData{nsCString(aLocation), false, 0, 271 mozilla::Nothing(), mozilla::Nothing()}) {} 272 FrameKeyFrameKey273 FrameKey(nsCString&& aLocation, bool aRelevantForJS, 274 uint64_t aInnerWindowID, const mozilla::Maybe<unsigned>& aLine, 275 const mozilla::Maybe<unsigned>& aColumn, 276 const mozilla::Maybe<JS::ProfilingCategoryPair>& aCategoryPair) 277 : mData(NormalFrameData{aLocation, aRelevantForJS, aInnerWindowID, 278 aLine, aColumn, aCategoryPair}) {} 279 FrameKeyFrameKey280 FrameKey(void* aJITAddress, uint32_t aJITDepth, uint32_t aRangeIndex) 281 : mData(JITFrameData{aJITAddress, aJITDepth, aRangeIndex}) {} 282 283 FrameKey(const FrameKey& aToCopy) = default; 284 285 uint32_t Hash() const; 286 bool operator==(const FrameKey& aOther) const { 287 return mData == aOther.mData; 288 } 289 290 struct NormalFrameData { 291 bool operator==(const NormalFrameData& aOther) const; 292 293 nsCString mLocation; 294 bool mRelevantForJS; 295 uint64_t mInnerWindowID; 296 mozilla::Maybe<unsigned> mLine; 297 mozilla::Maybe<unsigned> mColumn; 298 mozilla::Maybe<JS::ProfilingCategoryPair> mCategoryPair; 299 }; 300 struct JITFrameData { 301 bool operator==(const JITFrameData& aOther) const; 302 303 void* mCanonicalAddress; 304 uint32_t mDepth; 305 uint32_t mRangeIndex; 306 }; 307 mozilla::Variant<NormalFrameData, JITFrameData> mData; 308 }; 309 310 struct FrameKeyHasher { 311 using Lookup = FrameKey; 312 hashFrameKeyHasher313 static mozilla::HashNumber hash(const FrameKey& aLookup) { 314 mozilla::HashNumber hash = 0; 315 if (aLookup.mData.is<FrameKey::NormalFrameData>()) { 316 const FrameKey::NormalFrameData& data = 317 aLookup.mData.as<FrameKey::NormalFrameData>(); 318 if (!data.mLocation.IsEmpty()) { 319 hash = mozilla::AddToHash(hash, 320 mozilla::HashString(data.mLocation.get())); 321 } 322 hash = mozilla::AddToHash(hash, data.mRelevantForJS); 323 hash = mozilla::AddToHash(hash, data.mInnerWindowID); 324 if (data.mLine.isSome()) { 325 hash = mozilla::AddToHash(hash, *data.mLine); 326 } 327 if (data.mColumn.isSome()) { 328 hash = mozilla::AddToHash(hash, *data.mColumn); 329 } 330 if (data.mCategoryPair.isSome()) { 331 hash = mozilla::AddToHash(hash, 332 static_cast<uint32_t>(*data.mCategoryPair)); 333 } 334 } else { 335 const FrameKey::JITFrameData& data = 336 aLookup.mData.as<FrameKey::JITFrameData>(); 337 hash = mozilla::AddToHash(hash, data.mCanonicalAddress); 338 hash = mozilla::AddToHash(hash, data.mDepth); 339 hash = mozilla::AddToHash(hash, data.mRangeIndex); 340 } 341 return hash; 342 } 343 matchFrameKeyHasher344 static bool match(const FrameKey& aKey, const FrameKey& aLookup) { 345 return aKey == aLookup; 346 } 347 rekeyFrameKeyHasher348 static void rekey(FrameKey& aKey, const FrameKey& aNewKey) { 349 aKey = aNewKey; 350 } 351 }; 352 353 struct StackKey { 354 mozilla::Maybe<uint32_t> mPrefixStackIndex; 355 uint32_t mFrameIndex; 356 StackKeyStackKey357 explicit StackKey(uint32_t aFrame) 358 : mFrameIndex(aFrame), mHash(mozilla::HashGeneric(aFrame)) {} 359 StackKeyStackKey360 StackKey(const StackKey& aPrefix, uint32_t aPrefixStackIndex, 361 uint32_t aFrame) 362 : mPrefixStackIndex(mozilla::Some(aPrefixStackIndex)), 363 mFrameIndex(aFrame), 364 mHash(mozilla::AddToHash(aPrefix.mHash, aFrame)) {} 365 HashStackKey366 mozilla::HashNumber Hash() const { return mHash; } 367 368 bool operator==(const StackKey& aOther) const { 369 return mPrefixStackIndex == aOther.mPrefixStackIndex && 370 mFrameIndex == aOther.mFrameIndex; 371 } 372 373 private: 374 mozilla::HashNumber mHash; 375 }; 376 377 struct StackKeyHasher { 378 using Lookup = StackKey; 379 hashStackKeyHasher380 static mozilla::HashNumber hash(const StackKey& aLookup) { 381 return aLookup.Hash(); 382 } 383 matchStackKeyHasher384 static bool match(const StackKey& aKey, const StackKey& aLookup) { 385 return aKey == aLookup; 386 } 387 rekeyStackKeyHasher388 static void rekey(StackKey& aKey, const StackKey& aNewKey) { 389 aKey = aNewKey; 390 } 391 }; 392 393 explicit UniqueStacks(JITFrameInfo&& aJITFrameInfo); 394 395 // Return a StackKey for aFrame as the stack's root frame (no prefix). 396 [[nodiscard]] StackKey BeginStack(const FrameKey& aFrame); 397 398 // Return a new StackKey that is obtained by appending aFrame to aStack. 399 [[nodiscard]] StackKey AppendFrame(const StackKey& aStack, 400 const FrameKey& aFrame); 401 402 // Look up frame keys for the given JIT address, and ensure that our frame 403 // table has entries for the returned frame keys. The JSON for these frames 404 // is taken from mJITInfoRanges. 405 // aBufferPosition is needed in order to look up the correct JIT frame info 406 // object in mJITInfoRanges. 407 [[nodiscard]] mozilla::Maybe<mozilla::Vector<UniqueStacks::FrameKey>> 408 LookupFramesForJITAddressFromBufferPos(void* aJITAddress, 409 uint64_t aBufferPosition); 410 411 [[nodiscard]] uint32_t GetOrAddFrameIndex(const FrameKey& aFrame); 412 [[nodiscard]] uint32_t GetOrAddStackIndex(const StackKey& aStack); 413 414 void SpliceFrameTableElements(SpliceableJSONWriter& aWriter); 415 void SpliceStackTableElements(SpliceableJSONWriter& aWriter); 416 417 private: 418 void StreamNonJITFrame(const FrameKey& aFrame); 419 void StreamStack(const StackKey& aStack); 420 421 public: 422 mozilla::UniquePtr<UniqueJSONStrings> mUniqueStrings; 423 424 ProfilerCodeAddressService* mCodeAddressService = nullptr; 425 426 private: 427 SpliceableChunkedJSONWriter mFrameTableWriter; 428 mozilla::HashMap<FrameKey, uint32_t, FrameKeyHasher> mFrameToIndexMap; 429 430 SpliceableChunkedJSONWriter mStackTableWriter; 431 mozilla::HashMap<StackKey, uint32_t, StackKeyHasher> mStackToIndexMap; 432 433 mozilla::Vector<JITFrameInfoForBufferRange> mJITInfoRanges; 434 }; 435 436 // 437 // Thread profile JSON Format 438 // -------------------------- 439 // 440 // The profile contains much duplicate information. The output JSON of the 441 // profile attempts to deduplicate strings, frames, and stack prefixes, to cut 442 // down on size and to increase JSON streaming speed. Deduplicated values are 443 // streamed as indices into their respective tables. 444 // 445 // Further, arrays of objects with the same set of properties (e.g., samples, 446 // frames) are output as arrays according to a schema instead of an object 447 // with property names. A property that is not present is represented in the 448 // array as null or undefined. 449 // 450 // The format of the thread profile JSON is shown by the following example 451 // with 1 sample and 1 marker: 452 // 453 // { 454 // "name": "Foo", 455 // "tid": 42, 456 // "samples": 457 // { 458 // "schema": 459 // { 460 // "stack": 0, /* index into stackTable */ 461 // "time": 1, /* number */ 462 // "eventDelay": 2, /* number */ 463 // }, 464 // "data": 465 // [ 466 // [ 1, 0.0, 0.0 ] /* { stack: 1, time: 0.0, eventDelay: 0.0 } */ 467 // ] 468 // }, 469 // 470 // "markers": 471 // { 472 // "schema": 473 // { 474 // "name": 0, /* index into stringTable */ 475 // "time": 1, /* number */ 476 // "data": 2 /* arbitrary JSON */ 477 // }, 478 // "data": 479 // [ 480 // [ 3, 0.1 ] /* { name: 'example marker', time: 0.1 } */ 481 // ] 482 // }, 483 // 484 // "stackTable": 485 // { 486 // "schema": 487 // { 488 // "prefix": 0, /* index into stackTable */ 489 // "frame": 1 /* index into frameTable */ 490 // }, 491 // "data": 492 // [ 493 // [ null, 0 ], /* (root) */ 494 // [ 0, 1 ] /* (root) > foo.js */ 495 // ] 496 // }, 497 // 498 // "frameTable": 499 // { 500 // "schema": 501 // { 502 // "location": 0, /* index into stringTable */ 503 // "relevantForJS": 1, /* bool */ 504 // "innerWindowID": 2, /* inner window ID of global JS `window` object */ 505 // "implementation": 3, /* index into stringTable */ 506 // "optimizations": 4, /* arbitrary JSON */ 507 // "line": 5, /* number */ 508 // "column": 6, /* number */ 509 // "category": 7, /* index into profile.meta.categories */ 510 // "subcategory": 8 /* index into 511 // profile.meta.categories[category].subcategories */ 512 // }, 513 // "data": 514 // [ 515 // [ 0 ], /* { location: '(root)' } */ 516 // [ 1, 2 ] /* { location: 'foo.js', 517 // implementation: 'baseline' } */ 518 // ] 519 // }, 520 // 521 // "stringTable": 522 // [ 523 // "(root)", 524 // "foo.js", 525 // "baseline", 526 // "example marker" 527 // ] 528 // } 529 // 530 // Process: 531 // { 532 // "name": "Bar", 533 // "pid": 24, 534 // "threads": 535 // [ 536 // <0-N threads from above> 537 // ], 538 // "counters": /* includes the memory counter */ 539 // [ 540 // { 541 // "name": "qwerty", 542 // "category": "uiop", 543 // "description": "this is qwerty uiop", 544 // "sample_groups: 545 // [ 546 // { 547 // "id": 42, /* number (thread id, or object identifier (tab), etc) */ 548 // "samples: 549 // { 550 // "schema": 551 // { 552 // "time": 1, /* number */ 553 // "number": 2, /* number (of times the counter was touched) */ 554 // "count": 3 /* number (total for the counter) */ 555 // }, 556 // "data": 557 // [ 558 // [ 0.1, 1824, 559 // 454622 ] /* { time: 0.1, number: 1824, count: 454622 } */ 560 // ] 561 // }, 562 // }, 563 // /* more sample-group objects with different id's */ 564 // ] 565 // }, 566 // /* more counters */ 567 // ], 568 // } 569 // 570 #endif /* ndef ProfileBufferEntry_h */ 571