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