1 /*
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <fb303/ExportedStatMapImpl.h>
18
19 #include <fb303/DynamicCounters.h>
20 #include <gtest/gtest.h>
21 #include <atomic>
22 #include <thread>
23
24 #include <time.h>
25 using namespace std;
26 using namespace facebook;
27 using namespace facebook::fb303;
28
testExportedStatMapImpl(bool useStatPtr)29 void testExportedStatMapImpl(bool useStatPtr) {
30 DynamicCounters dc;
31 ExportedStatMapImpl statMap(&dc);
32
33 int64_t now = ::time(nullptr);
34 if (useStatPtr) {
35 ExportedStatMapImpl::StatPtr item = statMap.getStatPtr("test_value");
36 statMap.addValue(item, now, 10);
37 } else {
38 statMap.addValue("test_value", now, 10);
39 }
40
41 int64_t tmp;
42 EXPECT_FALSE(dc.getCounter("FAKE_test_value.avg.60", &tmp));
43
44 map<string, int64_t> res;
45 dc.getCounters(&res);
46 FOR_EACH (it, res) {
47 LOG(INFO) << "res[\"" << it->first << "\"] = " << it->second;
48 }
49 EXPECT_EQ(res.size(), 4);
50 EXPECT_EQ(res["test_value.avg.60"], 10);
51
52 EXPECT_TRUE(dc.getCounter("test_value.avg.60", &tmp));
53 EXPECT_EQ(tmp, 10);
54 // now remove the stat
55 statMap.unExportStatAll("test_value");
56 res.clear();
57 dc.getCounters(&res);
58 EXPECT_TRUE(res.empty());
59 }
60
TEST(ExportedStatMapImpl,ExportedBasics)61 TEST(ExportedStatMapImpl, ExportedBasics) {
62 testExportedStatMapImpl(false);
63 }
64
TEST(ExportedStatMapImpl,ExportedLockAndUpdate)65 TEST(ExportedStatMapImpl, ExportedLockAndUpdate) {
66 testExportedStatMapImpl(true);
67 }
68
69 // Destroy a timeseries stat without first unexporting its exported counters
TEST(ExportedStatMapImpl,ForgetWithoutUnexport)70 TEST(ExportedStatMapImpl, ForgetWithoutUnexport) {
71 DynamicCounters dc;
72 ExportedStatMapImpl statMap(&dc);
73
74 const string statName = "mystat";
75 statMap.addValue(statName, 0, 10);
76 statMap.addValue(statName, 0, 20);
77 statMap.forgetStatsFor(statName);
78
79 int64_t result;
80 string queryName = statName + ".avg";
81 dc.getCounter(queryName, &result);
82 EXPECT_EQ(result, 15);
83 }
84
85 // Have one thread continuously call forgetStatsFor(),
86 // while another thread is continuously updating (and re-exporting) the stat,
87 // and a third thread is querying the average value.
TEST(ExportedStatMapImpl,ConcurrentForget)88 TEST(ExportedStatMapImpl, ConcurrentForget) {
89 DynamicCounters dc;
90 ExportedStatMapImpl statMap(&dc);
91
92 const string statName = "mystat";
93
94 uint64_t updateIterations = 0;
95 uint64_t forgetIterations = 0;
96 uint64_t queryIterations = 0;
97
98 // Run for 2 seconds
99 auto end = std::chrono::steady_clock::now() + std::chrono::seconds(2);
100 std::atomic<bool> done(false);
101 std::thread updateThread([&] {
102 while (std::chrono::steady_clock::now() < end) {
103 for (int n = 0; n < 100; ++n) {
104 statMap.addValue(statName, 0, 1);
105 ++updateIterations;
106 }
107 }
108 done = true;
109 });
110
111 std::thread forgetThread([&] {
112 while (!done) {
113 statMap.forgetStatsFor(statName);
114 ++forgetIterations;
115 }
116 });
117
118 std::thread queryThread([&] {
119 string queryName = statName + ".avg";
120 while (!done) {
121 int64_t result;
122 dc.getCounter(queryName, &result);
123 ++queryIterations;
124 }
125 });
126
127 updateThread.join();
128 forgetThread.join();
129 queryThread.join();
130
131 // Just for sanity, make sure each thread ran through some
132 // minimal number of iterations.
133 EXPECT_GT(updateIterations, 1000);
134 EXPECT_GT(forgetIterations, 1000);
135 EXPECT_GT(queryIterations, 1000);
136 }
137
exportStatThread(ExportedStatMapImpl * statsMap,const std::string & counterName,uint32_t numIters,uint64_t incrAmount)138 void exportStatThread(
139 ExportedStatMapImpl* statsMap,
140 const std::string& counterName,
141 uint32_t numIters,
142 uint64_t incrAmount) {
143 for (uint32_t n = 0; n < numIters; ++n) {
144 statsMap->exportStat(counterName, fb303::SUM);
145 statsMap->addValue(counterName, ::time(nullptr), incrAmount);
146 sched_yield();
147 }
148 }
149
150 // Test calling exportStat() simultaneously from multiple threads,
151 // while the stat is also being updated.
TEST(ExportedStatMapImpl,MultithreadedExport)152 TEST(ExportedStatMapImpl, MultithreadedExport) {
153 DynamicCounters counters;
154 ExportedStatMapImpl statsMap(&counters);
155
156 std::string counterName = "foo";
157 uint32_t numIters = 1000;
158 uint32_t numThreads = 4;
159 uint64_t incrAmount = 8;
160
161 std::vector<std::thread> threads;
162 for (uint32_t n = 0; n < numThreads; ++n) {
163 threads.emplace_back(
164 exportStatThread, &statsMap, counterName, numIters, incrAmount);
165 }
166 for (auto& thread : threads) {
167 thread.join();
168 }
169
170 auto lockedObj = statsMap.getLockedStatPtr(counterName);
171 EXPECT_EQ(numIters * numThreads * incrAmount, lockedObj->sum(0));
172 }
173
TEST(LockableStat,Swap)174 TEST(LockableStat, Swap) {
175 using LockableStat = ExportedStatMapImpl::LockableStat;
176 DynamicCounters dc;
177 ExportedStatMapImpl statMap(&dc);
178
179 LockableStat statA = statMap.getLockableStat("testA");
180 {
181 auto guard = statA.lock();
182 statA.addValueLocked(guard, ::time(nullptr), 10);
183 statA.flushLocked(guard);
184 }
185 LockableStat statB = statMap.getLockableStat("testB");
186
187 {
188 auto guard = statA.lock();
189 EXPECT_EQ(guard->sum(0), 10);
190 guard = statB.lock();
191 EXPECT_EQ(guard->sum(0), 0);
192 }
193
194 statA.swap(statB);
195
196 // the stats are reversed now
197 // so acquire the locks in the B/A order
198 // to avoid a TSAN lock-order-inversion message
199 auto guard = statB.lock();
200 EXPECT_EQ(guard->sum(0), 10);
201 guard = statA.lock();
202 EXPECT_EQ(guard->sum(0), 0);
203 }
204
TEST(LockableStat,AddValue)205 TEST(LockableStat, AddValue) {
206 using LockableStat = ExportedStatMapImpl::LockableStat;
207 DynamicCounters dc;
208 ExportedStatMapImpl statMap(&dc);
209
210 time_t now = ::time(nullptr);
211 const string name = "test_value";
212 LockableStat stat = statMap.getLockableStat(name);
213 stat.addValue(now, 10);
214
215 int64_t result;
216 string queryname = name + ".avg";
217 dc.getCounter(queryname, &result);
218 EXPECT_EQ(result, 10);
219 {
220 auto guard = stat.lock();
221 stat.addValueLocked(guard, now, 20);
222 }
223 dc.getCounter(queryname, &result);
224 EXPECT_EQ(result, 15);
225 }
226