1 //===- unittest/ProfileData/SampleProfTest.cpp ------------------*- C++ -*-===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "llvm/ProfileData/SampleProf.h" 10 #include "llvm/ADT/StringMap.h" 11 #include "llvm/ADT/StringRef.h" 12 #include "llvm/IR/LLVMContext.h" 13 #include "llvm/IR/Metadata.h" 14 #include "llvm/IR/Module.h" 15 #include "llvm/ProfileData/SampleProfReader.h" 16 #include "llvm/ProfileData/SampleProfWriter.h" 17 #include "llvm/Support/Casting.h" 18 #include "llvm/Support/ErrorOr.h" 19 #include "llvm/Support/FileSystem.h" 20 #include "llvm/Support/MemoryBuffer.h" 21 #include "llvm/Support/raw_ostream.h" 22 #include "llvm/Testing/Support/SupportHelpers.h" 23 #include "gtest/gtest.h" 24 #include <string> 25 #include <vector> 26 27 using namespace llvm; 28 using namespace sampleprof; 29 30 using llvm::unittest::TempFile; 31 32 static ::testing::AssertionResult NoError(std::error_code EC) { 33 if (!EC) 34 return ::testing::AssertionSuccess(); 35 return ::testing::AssertionFailure() << "error " << EC.value() << ": " 36 << EC.message(); 37 } 38 39 namespace { 40 41 struct SampleProfTest : ::testing::Test { 42 LLVMContext Context; 43 std::unique_ptr<SampleProfileWriter> Writer; 44 std::unique_ptr<SampleProfileReader> Reader; 45 46 SampleProfTest() : Writer(), Reader() {} 47 48 void createWriter(SampleProfileFormat Format, StringRef Profile) { 49 std::error_code EC; 50 std::unique_ptr<raw_ostream> OS( 51 new raw_fd_ostream(Profile, EC, sys::fs::OF_None)); 52 auto WriterOrErr = SampleProfileWriter::create(OS, Format); 53 ASSERT_TRUE(NoError(WriterOrErr.getError())); 54 Writer = std::move(WriterOrErr.get()); 55 } 56 57 void readProfile(const Module &M, StringRef Profile, 58 StringRef RemapFile = "") { 59 auto ReaderOrErr = SampleProfileReader::create( 60 std::string(Profile), Context, std::string(RemapFile)); 61 ASSERT_TRUE(NoError(ReaderOrErr.getError())); 62 Reader = std::move(ReaderOrErr.get()); 63 Reader->setModule(&M); 64 } 65 66 TempFile createRemapFile() { 67 return TempFile("remapfile", "", R"( 68 # Types 'int' and 'long' are equivalent 69 type i l 70 # Function names 'foo' and 'faux' are equivalent 71 name 3foo 4faux 72 )", 73 /*Unique*/ true); 74 } 75 76 // Verify profile summary is consistent in the roundtrip to and from 77 // Metadata. \p AddPartialField is to choose whether the Metadata 78 // contains the IsPartialProfile field which is optional. 79 void verifyProfileSummary(ProfileSummary &Summary, Module &M, 80 const bool AddPartialField, 81 const bool AddPartialProfileRatioField) { 82 LLVMContext &Context = M.getContext(); 83 const bool IsPartialProfile = Summary.isPartialProfile(); 84 const double PartialProfileRatio = Summary.getPartialProfileRatio(); 85 auto VerifySummary = [IsPartialProfile, PartialProfileRatio]( 86 ProfileSummary &Summary) mutable { 87 ASSERT_EQ(ProfileSummary::PSK_Sample, Summary.getKind()); 88 ASSERT_EQ(138211u, Summary.getTotalCount()); 89 ASSERT_EQ(10u, Summary.getNumCounts()); 90 ASSERT_EQ(4u, Summary.getNumFunctions()); 91 ASSERT_EQ(1437u, Summary.getMaxFunctionCount()); 92 ASSERT_EQ(60351u, Summary.getMaxCount()); 93 ASSERT_EQ(IsPartialProfile, Summary.isPartialProfile()); 94 ASSERT_EQ(PartialProfileRatio, Summary.getPartialProfileRatio()); 95 96 uint32_t Cutoff = 800000; 97 auto Predicate = [&Cutoff](const ProfileSummaryEntry &PE) { 98 return PE.Cutoff == Cutoff; 99 }; 100 std::vector<ProfileSummaryEntry> &Details = Summary.getDetailedSummary(); 101 auto EightyPerc = find_if(Details, Predicate); 102 Cutoff = 900000; 103 auto NinetyPerc = find_if(Details, Predicate); 104 Cutoff = 950000; 105 auto NinetyFivePerc = find_if(Details, Predicate); 106 Cutoff = 990000; 107 auto NinetyNinePerc = find_if(Details, Predicate); 108 ASSERT_EQ(60000u, EightyPerc->MinCount); 109 ASSERT_EQ(12557u, NinetyPerc->MinCount); 110 ASSERT_EQ(12557u, NinetyFivePerc->MinCount); 111 ASSERT_EQ(600u, NinetyNinePerc->MinCount); 112 }; 113 VerifySummary(Summary); 114 115 // Test that conversion of summary to and from Metadata works. 116 Metadata *MD = 117 Summary.getMD(Context, AddPartialField, AddPartialProfileRatioField); 118 ASSERT_TRUE(MD); 119 ProfileSummary *PS = ProfileSummary::getFromMD(MD); 120 ASSERT_TRUE(PS); 121 VerifySummary(*PS); 122 delete PS; 123 124 // Test that summary can be attached to and read back from module. 125 M.eraseNamedMetadata(M.getOrInsertModuleFlagsMetadata()); 126 M.setProfileSummary(MD, ProfileSummary::PSK_Sample); 127 MD = M.getProfileSummary(/* IsCS */ false); 128 ASSERT_TRUE(MD); 129 PS = ProfileSummary::getFromMD(MD); 130 ASSERT_TRUE(PS); 131 VerifySummary(*PS); 132 delete PS; 133 } 134 135 void testRoundTrip(SampleProfileFormat Format, bool Remap, bool UseMD5) { 136 TempFile ProfileFile("profile", "", "", /*Unique*/ true); 137 createWriter(Format, ProfileFile.path()); 138 if (Format == SampleProfileFormat::SPF_Ext_Binary && UseMD5) 139 static_cast<SampleProfileWriterExtBinary *>(Writer.get())->setUseMD5(); 140 141 StringRef FooName("_Z3fooi"); 142 FunctionSamples FooSamples; 143 FooSamples.setName(FooName); 144 FooSamples.addTotalSamples(7711); 145 FooSamples.addHeadSamples(610); 146 FooSamples.addBodySamples(1, 0, 610); 147 FooSamples.addBodySamples(2, 0, 600); 148 FooSamples.addBodySamples(4, 0, 60000); 149 FooSamples.addBodySamples(8, 0, 60351); 150 FooSamples.addBodySamples(10, 0, 605); 151 152 // Add inline instance with name "_Z3gooi". 153 StringRef GooName("_Z3gooi"); 154 auto &GooSamples = 155 FooSamples.functionSamplesAt(LineLocation(7, 0))[GooName.str()]; 156 GooSamples.setName(GooName); 157 GooSamples.addTotalSamples(502); 158 GooSamples.addBodySamples(3, 0, 502); 159 160 // Add inline instance with name "_Z3hooi". 161 StringRef HooName("_Z3hooi"); 162 auto &HooSamples = 163 GooSamples.functionSamplesAt(LineLocation(9, 0))[HooName.str()]; 164 HooSamples.setName(HooName); 165 HooSamples.addTotalSamples(317); 166 HooSamples.addBodySamples(4, 0, 317); 167 168 StringRef BarName("_Z3bari"); 169 FunctionSamples BarSamples; 170 BarSamples.setName(BarName); 171 BarSamples.addTotalSamples(20301); 172 BarSamples.addHeadSamples(1437); 173 BarSamples.addBodySamples(1, 0, 1437); 174 // Test how reader/writer handles unmangled names. 175 StringRef MconstructName("_M_construct<char *>"); 176 StringRef StringviewName("string_view<std::allocator<char> >"); 177 BarSamples.addCalledTargetSamples(1, 0, MconstructName, 1000); 178 BarSamples.addCalledTargetSamples(1, 0, StringviewName, 437); 179 180 StringRef BazName("_Z3bazi"); 181 FunctionSamples BazSamples; 182 BazSamples.setName(BazName); 183 BazSamples.addTotalSamples(12557); 184 BazSamples.addHeadSamples(1257); 185 BazSamples.addBodySamples(1, 0, 12557); 186 187 StringRef BooName("_Z3booi"); 188 FunctionSamples BooSamples; 189 BooSamples.setName(BooName); 190 BooSamples.addTotalSamples(1232); 191 BooSamples.addHeadSamples(1); 192 BooSamples.addBodySamples(1, 0, 1232); 193 194 StringMap<FunctionSamples> Profiles; 195 Profiles[FooName] = std::move(FooSamples); 196 Profiles[BarName] = std::move(BarSamples); 197 Profiles[BazName] = std::move(BazSamples); 198 Profiles[BooName] = std::move(BooSamples); 199 200 Module M("my_module", Context); 201 FunctionType *fn_type = 202 FunctionType::get(Type::getVoidTy(Context), {}, false); 203 204 TempFile RemapFile(createRemapFile()); 205 if (Remap) { 206 FooName = "_Z4fauxi"; 207 BarName = "_Z3barl"; 208 GooName = "_Z3gool"; 209 HooName = "_Z3hool"; 210 } 211 212 M.getOrInsertFunction(FooName, fn_type); 213 M.getOrInsertFunction(BarName, fn_type); 214 M.getOrInsertFunction(BooName, fn_type); 215 216 ProfileSymbolList List; 217 if (Format == SampleProfileFormat::SPF_Ext_Binary) { 218 List.add("zoo", true); 219 List.add("moo", true); 220 } 221 Writer->setProfileSymbolList(&List); 222 223 std::error_code EC; 224 EC = Writer->write(Profiles); 225 ASSERT_TRUE(NoError(EC)); 226 227 Writer->getOutputStream().flush(); 228 229 readProfile(M, ProfileFile.path(), RemapFile.path()); 230 EC = Reader->read(); 231 ASSERT_TRUE(NoError(EC)); 232 233 if (Format == SampleProfileFormat::SPF_Ext_Binary) { 234 std::unique_ptr<ProfileSymbolList> ReaderList = 235 Reader->getProfileSymbolList(); 236 ReaderList->contains("zoo"); 237 ReaderList->contains("moo"); 238 } 239 240 FunctionSamples *ReadFooSamples = Reader->getSamplesFor(FooName); 241 ASSERT_TRUE(ReadFooSamples != nullptr); 242 if (!UseMD5) { 243 ASSERT_EQ("_Z3fooi", ReadFooSamples->getName()); 244 } 245 ASSERT_EQ(7711u, ReadFooSamples->getTotalSamples()); 246 ASSERT_EQ(610u, ReadFooSamples->getHeadSamples()); 247 248 // Try to find a FunctionSamples with GooName at given callsites containing 249 // inline instance for GooName. Test the correct FunctionSamples can be 250 // found with Remapper support. 251 const FunctionSamples *ReadGooSamples = 252 ReadFooSamples->findFunctionSamplesAt(LineLocation(7, 0), GooName, 253 Reader->getRemapper()); 254 ASSERT_TRUE(ReadGooSamples != nullptr); 255 ASSERT_EQ(502u, ReadGooSamples->getTotalSamples()); 256 257 // Try to find a FunctionSamples with GooName at given callsites containing 258 // no inline instance for GooName. Test no FunctionSamples will be 259 // found with Remapper support. 260 const FunctionSamples *ReadGooSamplesAgain = 261 ReadFooSamples->findFunctionSamplesAt(LineLocation(9, 0), GooName, 262 Reader->getRemapper()); 263 ASSERT_TRUE(ReadGooSamplesAgain == nullptr); 264 265 // The inline instance of Hoo is inside of the inline instance of Goo. 266 // Try to find a FunctionSamples with HooName at given callsites containing 267 // inline instance for HooName. Test the correct FunctionSamples can be 268 // found with Remapper support. 269 const FunctionSamples *ReadHooSamples = 270 ReadGooSamples->findFunctionSamplesAt(LineLocation(9, 0), HooName, 271 Reader->getRemapper()); 272 ASSERT_TRUE(ReadHooSamples != nullptr); 273 ASSERT_EQ(317u, ReadHooSamples->getTotalSamples()); 274 275 FunctionSamples *ReadBarSamples = Reader->getSamplesFor(BarName); 276 ASSERT_TRUE(ReadBarSamples != nullptr); 277 if (!UseMD5) { 278 ASSERT_EQ("_Z3bari", ReadBarSamples->getName()); 279 } 280 ASSERT_EQ(20301u, ReadBarSamples->getTotalSamples()); 281 ASSERT_EQ(1437u, ReadBarSamples->getHeadSamples()); 282 ErrorOr<SampleRecord::CallTargetMap> CTMap = 283 ReadBarSamples->findCallTargetMapAt(1, 0); 284 ASSERT_FALSE(CTMap.getError()); 285 286 // Because _Z3bazi is not defined in module M, expect _Z3bazi's profile 287 // is not loaded when the profile is ExtBinary or Compact format because 288 // these formats support loading function profiles on demand. 289 FunctionSamples *ReadBazSamples = Reader->getSamplesFor(BazName); 290 if (Format == SampleProfileFormat::SPF_Ext_Binary || 291 Format == SampleProfileFormat::SPF_Compact_Binary) { 292 ASSERT_TRUE(ReadBazSamples == nullptr); 293 ASSERT_EQ(3u, Reader->getProfiles().size()); 294 } else { 295 ASSERT_TRUE(ReadBazSamples != nullptr); 296 ASSERT_EQ(12557u, ReadBazSamples->getTotalSamples()); 297 ASSERT_EQ(4u, Reader->getProfiles().size()); 298 } 299 300 FunctionSamples *ReadBooSamples = Reader->getSamplesFor(BooName); 301 ASSERT_TRUE(ReadBooSamples != nullptr); 302 ASSERT_EQ(1232u, ReadBooSamples->getTotalSamples()); 303 304 std::string MconstructGUID; 305 StringRef MconstructRep = 306 getRepInFormat(MconstructName, UseMD5, MconstructGUID); 307 std::string StringviewGUID; 308 StringRef StringviewRep = 309 getRepInFormat(StringviewName, UseMD5, StringviewGUID); 310 ASSERT_EQ(1000u, CTMap.get()[MconstructRep]); 311 ASSERT_EQ(437u, CTMap.get()[StringviewRep]); 312 313 314 ProfileSummary &Summary = Reader->getSummary(); 315 Summary.setPartialProfile(true); 316 verifyProfileSummary(Summary, M, true, false); 317 318 Summary.setPartialProfile(false); 319 verifyProfileSummary(Summary, M, true, false); 320 321 verifyProfileSummary(Summary, M, false, false); 322 323 Summary.setPartialProfile(true); 324 Summary.setPartialProfileRatio(0.5); 325 verifyProfileSummary(Summary, M, true, true); 326 } 327 328 void addFunctionSamples(StringMap<FunctionSamples> *Smap, const char *Fname, 329 uint64_t TotalSamples, uint64_t HeadSamples) { 330 StringRef Name(Fname); 331 FunctionSamples FcnSamples; 332 FcnSamples.setName(Name); 333 FcnSamples.addTotalSamples(TotalSamples); 334 FcnSamples.addHeadSamples(HeadSamples); 335 FcnSamples.addBodySamples(1, 0, HeadSamples); 336 (*Smap)[Name] = FcnSamples; 337 } 338 339 StringMap<FunctionSamples> setupFcnSamplesForElisionTest(StringRef Policy) { 340 StringMap<FunctionSamples> Smap; 341 addFunctionSamples(&Smap, "foo", uint64_t(20301), uint64_t(1437)); 342 if (Policy == "" || Policy == "all") 343 return Smap; 344 addFunctionSamples(&Smap, "foo.bar", uint64_t(20303), uint64_t(1439)); 345 if (Policy == "selected") 346 return Smap; 347 addFunctionSamples(&Smap, "foo.llvm.2465", uint64_t(20305), uint64_t(1441)); 348 return Smap; 349 } 350 351 void createFunctionWithSampleProfileElisionPolicy(Module *M, 352 const char *Fname, 353 StringRef Policy) { 354 FunctionType *FnType = 355 FunctionType::get(Type::getVoidTy(Context), {}, false); 356 auto Inserted = M->getOrInsertFunction(Fname, FnType); 357 auto Fcn = cast<Function>(Inserted.getCallee()); 358 if (Policy != "") 359 Fcn->addFnAttr("sample-profile-suffix-elision-policy", Policy); 360 } 361 362 void setupModuleForElisionTest(Module *M, StringRef Policy) { 363 createFunctionWithSampleProfileElisionPolicy(M, "foo", Policy); 364 createFunctionWithSampleProfileElisionPolicy(M, "foo.bar", Policy); 365 createFunctionWithSampleProfileElisionPolicy(M, "foo.llvm.2465", Policy); 366 } 367 368 void testSuffixElisionPolicy(SampleProfileFormat Format, StringRef Policy, 369 const StringMap<uint64_t> &Expected) { 370 TempFile ProfileFile("profile", "", "", /*Unique*/ true); 371 372 Module M("my_module", Context); 373 setupModuleForElisionTest(&M, Policy); 374 StringMap<FunctionSamples> ProfMap = setupFcnSamplesForElisionTest(Policy); 375 376 // write profile 377 createWriter(Format, ProfileFile.path()); 378 std::error_code EC; 379 EC = Writer->write(ProfMap); 380 ASSERT_TRUE(NoError(EC)); 381 Writer->getOutputStream().flush(); 382 383 // read profile 384 readProfile(M, ProfileFile.path()); 385 EC = Reader->read(); 386 ASSERT_TRUE(NoError(EC)); 387 388 for (auto I = Expected.begin(); I != Expected.end(); ++I) { 389 uint64_t Esamples = uint64_t(-1); 390 FunctionSamples *Samples = Reader->getSamplesFor(I->getKey()); 391 if (Samples != nullptr) 392 Esamples = Samples->getTotalSamples(); 393 ASSERT_EQ(I->getValue(), Esamples); 394 } 395 } 396 }; 397 398 TEST_F(SampleProfTest, roundtrip_text_profile) { 399 testRoundTrip(SampleProfileFormat::SPF_Text, false, false); 400 } 401 402 TEST_F(SampleProfTest, roundtrip_raw_binary_profile) { 403 testRoundTrip(SampleProfileFormat::SPF_Binary, false, false); 404 } 405 406 TEST_F(SampleProfTest, roundtrip_compact_binary_profile) { 407 testRoundTrip(SampleProfileFormat::SPF_Compact_Binary, false, true); 408 } 409 410 TEST_F(SampleProfTest, roundtrip_ext_binary_profile) { 411 testRoundTrip(SampleProfileFormat::SPF_Ext_Binary, false, false); 412 } 413 414 TEST_F(SampleProfTest, roundtrip_md5_ext_binary_profile) { 415 testRoundTrip(SampleProfileFormat::SPF_Ext_Binary, false, true); 416 } 417 418 TEST_F(SampleProfTest, remap_text_profile) { 419 testRoundTrip(SampleProfileFormat::SPF_Text, true, false); 420 } 421 422 TEST_F(SampleProfTest, remap_raw_binary_profile) { 423 testRoundTrip(SampleProfileFormat::SPF_Binary, true, false); 424 } 425 426 TEST_F(SampleProfTest, remap_ext_binary_profile) { 427 testRoundTrip(SampleProfileFormat::SPF_Ext_Binary, true, false); 428 } 429 430 TEST_F(SampleProfTest, sample_overflow_saturation) { 431 const uint64_t Max = std::numeric_limits<uint64_t>::max(); 432 sampleprof_error Result; 433 434 FunctionSamples FooSamples; 435 Result = FooSamples.addTotalSamples(1); 436 ASSERT_EQ(Result, sampleprof_error::success); 437 438 Result = FooSamples.addHeadSamples(1); 439 ASSERT_EQ(Result, sampleprof_error::success); 440 441 Result = FooSamples.addBodySamples(10, 0, 1); 442 ASSERT_EQ(Result, sampleprof_error::success); 443 444 Result = FooSamples.addTotalSamples(Max); 445 ASSERT_EQ(Result, sampleprof_error::counter_overflow); 446 ASSERT_EQ(FooSamples.getTotalSamples(), Max); 447 448 Result = FooSamples.addHeadSamples(Max); 449 ASSERT_EQ(Result, sampleprof_error::counter_overflow); 450 ASSERT_EQ(FooSamples.getHeadSamples(), Max); 451 452 Result = FooSamples.addBodySamples(10, 0, Max); 453 ASSERT_EQ(Result, sampleprof_error::counter_overflow); 454 ErrorOr<uint64_t> BodySamples = FooSamples.findSamplesAt(10, 0); 455 ASSERT_FALSE(BodySamples.getError()); 456 ASSERT_EQ(BodySamples.get(), Max); 457 } 458 459 TEST_F(SampleProfTest, default_suffix_elision_text) { 460 // Default suffix elision policy: strip everything after first dot. 461 // This implies that all suffix variants will map to "foo", so 462 // we don't expect to see any entries for them in the sample 463 // profile. 464 StringMap<uint64_t> Expected; 465 Expected["foo"] = uint64_t(20301); 466 Expected["foo.bar"] = uint64_t(-1); 467 Expected["foo.llvm.2465"] = uint64_t(-1); 468 testSuffixElisionPolicy(SampleProfileFormat::SPF_Text, "", Expected); 469 } 470 471 TEST_F(SampleProfTest, default_suffix_elision_compact_binary) { 472 // Default suffix elision policy: strip everything after first dot. 473 // This implies that all suffix variants will map to "foo", so 474 // we don't expect to see any entries for them in the sample 475 // profile. 476 StringMap<uint64_t> Expected; 477 Expected["foo"] = uint64_t(20301); 478 Expected["foo.bar"] = uint64_t(-1); 479 Expected["foo.llvm.2465"] = uint64_t(-1); 480 testSuffixElisionPolicy(SampleProfileFormat::SPF_Compact_Binary, "", 481 Expected); 482 } 483 484 TEST_F(SampleProfTest, selected_suffix_elision_text) { 485 // Profile is created and searched using the "selected" 486 // suffix elision policy: we only strip a .XXX suffix if 487 // it matches a pattern known to be generated by the compiler 488 // (e.g. ".llvm.<digits>"). 489 StringMap<uint64_t> Expected; 490 Expected["foo"] = uint64_t(20301); 491 Expected["foo.bar"] = uint64_t(20303); 492 Expected["foo.llvm.2465"] = uint64_t(-1); 493 testSuffixElisionPolicy(SampleProfileFormat::SPF_Text, "selected", Expected); 494 } 495 496 TEST_F(SampleProfTest, selected_suffix_elision_compact_binary) { 497 // Profile is created and searched using the "selected" 498 // suffix elision policy: we only strip a .XXX suffix if 499 // it matches a pattern known to be generated by the compiler 500 // (e.g. ".llvm.<digits>"). 501 StringMap<uint64_t> Expected; 502 Expected["foo"] = uint64_t(20301); 503 Expected["foo.bar"] = uint64_t(20303); 504 Expected["foo.llvm.2465"] = uint64_t(-1); 505 testSuffixElisionPolicy(SampleProfileFormat::SPF_Compact_Binary, "selected", 506 Expected); 507 } 508 509 TEST_F(SampleProfTest, none_suffix_elision_text) { 510 // Profile is created and searched using the "none" 511 // suffix elision policy: no stripping of suffixes at all. 512 // Here we expect to see all variants in the profile. 513 StringMap<uint64_t> Expected; 514 Expected["foo"] = uint64_t(20301); 515 Expected["foo.bar"] = uint64_t(20303); 516 Expected["foo.llvm.2465"] = uint64_t(20305); 517 testSuffixElisionPolicy(SampleProfileFormat::SPF_Text, "none", Expected); 518 } 519 520 TEST_F(SampleProfTest, none_suffix_elision_compact_binary) { 521 // Profile is created and searched using the "none" 522 // suffix elision policy: no stripping of suffixes at all. 523 // Here we expect to see all variants in the profile. 524 StringMap<uint64_t> Expected; 525 Expected["foo"] = uint64_t(20301); 526 Expected["foo.bar"] = uint64_t(20303); 527 Expected["foo.llvm.2465"] = uint64_t(20305); 528 testSuffixElisionPolicy(SampleProfileFormat::SPF_Compact_Binary, "none", 529 Expected); 530 } 531 532 } // end anonymous namespace 533