blob: 311042756f3dae353308cf2341eef7f1c829aea9 [file] [log] [blame]
Aart Bikd14c5952015-09-08 15:25:15 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
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 <limits.h>
18
19#include "induction_var_range.h"
20
21namespace art {
22
Aart Bikb3365e02015-09-21 14:45:05 -070023/** Returns true if 64-bit constant fits in 32-bit constant. */
24static bool CanLongValueFitIntoInt(int64_t c) {
25 return INT_MIN <= c && c <= INT_MAX;
Aart Bikd14c5952015-09-08 15:25:15 -070026}
27
Aart Bikb3365e02015-09-21 14:45:05 -070028/** Returns true if 32-bit addition can be done safely. */
Aart Bikd14c5952015-09-08 15:25:15 -070029static bool IsSafeAdd(int32_t c1, int32_t c2) {
Aart Bikb3365e02015-09-21 14:45:05 -070030 return CanLongValueFitIntoInt(static_cast<int64_t>(c1) + static_cast<int64_t>(c2));
Aart Bikd14c5952015-09-08 15:25:15 -070031}
32
Aart Bikb3365e02015-09-21 14:45:05 -070033/** Returns true if 32-bit subtraction can be done safely. */
Aart Bikd14c5952015-09-08 15:25:15 -070034static bool IsSafeSub(int32_t c1, int32_t c2) {
Aart Bikb3365e02015-09-21 14:45:05 -070035 return CanLongValueFitIntoInt(static_cast<int64_t>(c1) - static_cast<int64_t>(c2));
Aart Bikd14c5952015-09-08 15:25:15 -070036}
37
Aart Bikb3365e02015-09-21 14:45:05 -070038/** Returns true if 32-bit multiplication can be done safely. */
Aart Bikd14c5952015-09-08 15:25:15 -070039static bool IsSafeMul(int32_t c1, int32_t c2) {
Aart Bikb3365e02015-09-21 14:45:05 -070040 return CanLongValueFitIntoInt(static_cast<int64_t>(c1) * static_cast<int64_t>(c2));
Aart Bikd14c5952015-09-08 15:25:15 -070041}
42
Aart Bikb3365e02015-09-21 14:45:05 -070043/** Returns true if 32-bit division can be done safely. */
Aart Bikd14c5952015-09-08 15:25:15 -070044static bool IsSafeDiv(int32_t c1, int32_t c2) {
Aart Bikb3365e02015-09-21 14:45:05 -070045 return c2 != 0 && CanLongValueFitIntoInt(static_cast<int64_t>(c1) / static_cast<int64_t>(c2));
Aart Bikd14c5952015-09-08 15:25:15 -070046}
47
Aart Bikb3365e02015-09-21 14:45:05 -070048/** Returns true for 32/64-bit integral constant. */
Aart Bikd14c5952015-09-08 15:25:15 -070049static bool IsIntAndGet(HInstruction* instruction, int32_t* value) {
50 if (instruction->IsIntConstant()) {
Aart Bikb3365e02015-09-21 14:45:05 -070051 *value = instruction->AsIntConstant()->GetValue();
52 return true;
Aart Bikd14c5952015-09-08 15:25:15 -070053 } else if (instruction->IsLongConstant()) {
54 const int64_t c = instruction->AsLongConstant()->GetValue();
Aart Bikb3365e02015-09-21 14:45:05 -070055 if (CanLongValueFitIntoInt(c)) {
56 *value = static_cast<int32_t>(c);
Aart Bikd14c5952015-09-08 15:25:15 -070057 return true;
58 }
59 }
60 return false;
61}
62
Aart Bikb3365e02015-09-21 14:45:05 -070063/**
64 * An upper bound a * (length / a) + b, where a > 0, can be conservatively rewritten as length + b
65 * because length >= 0 is true. This makes it more likely the bound is useful to clients.
66 */
67static InductionVarRange::Value SimplifyMax(InductionVarRange::Value v) {
68 int32_t value;
69 if (v.a_constant > 1 &&
70 v.instruction->IsDiv() &&
71 v.instruction->InputAt(0)->IsArrayLength() &&
72 IsIntAndGet(v.instruction->InputAt(1), &value) && v.a_constant == value) {
73 return InductionVarRange::Value(v.instruction->InputAt(0), 1, v.b_constant);
74 }
75 return v;
76}
77
Aart Bikd14c5952015-09-08 15:25:15 -070078//
79// Public class methods.
80//
81
82InductionVarRange::InductionVarRange(HInductionVarAnalysis* induction_analysis)
83 : induction_analysis_(induction_analysis) {
Aart Bikb3365e02015-09-21 14:45:05 -070084 DCHECK(induction_analysis != nullptr);
Aart Bikd14c5952015-09-08 15:25:15 -070085}
86
87InductionVarRange::Value InductionVarRange::GetMinInduction(HInstruction* context,
88 HInstruction* instruction) {
89 HLoopInformation* loop = context->GetBlock()->GetLoopInformation();
Aart Bikb3365e02015-09-21 14:45:05 -070090 if (loop != nullptr) {
Aart Bikd14c5952015-09-08 15:25:15 -070091 return GetMin(induction_analysis_->LookupInfo(loop, instruction), GetTripCount(loop, context));
92 }
Aart Bikb3365e02015-09-21 14:45:05 -070093 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -070094}
95
96InductionVarRange::Value InductionVarRange::GetMaxInduction(HInstruction* context,
97 HInstruction* instruction) {
98 HLoopInformation* loop = context->GetBlock()->GetLoopInformation();
Aart Bikb3365e02015-09-21 14:45:05 -070099 if (loop != nullptr) {
100 return SimplifyMax(
101 GetMax(induction_analysis_->LookupInfo(loop, instruction), GetTripCount(loop, context)));
Aart Bikd14c5952015-09-08 15:25:15 -0700102 }
Aart Bikb3365e02015-09-21 14:45:05 -0700103 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700104}
105
106//
107// Private class methods.
108//
109
110HInductionVarAnalysis::InductionInfo* InductionVarRange::GetTripCount(HLoopInformation* loop,
111 HInstruction* context) {
112 // The trip-count expression is only valid when the top-test is taken at least once,
113 // that means, when the analyzed context appears outside the loop header itself.
114 // Early-exit loops are okay, since in those cases, the trip-count is conservative.
Aart Bikb3365e02015-09-21 14:45:05 -0700115 //
116 // TODO: deal with runtime safety issues on TCs
117 //
Aart Bikd14c5952015-09-08 15:25:15 -0700118 if (context->GetBlock() != loop->GetHeader()) {
119 HInductionVarAnalysis::InductionInfo* trip =
120 induction_analysis_->LookupInfo(loop, loop->GetHeader()->GetLastInstruction());
121 if (trip != nullptr) {
122 // Wrap the trip-count representation in its own unusual NOP node, so that range analysis
123 // is able to determine the [0, TC - 1] interval without having to construct constants.
124 return induction_analysis_->CreateInvariantOp(HInductionVarAnalysis::kNop, trip, trip);
125 }
126 }
127 return nullptr;
128}
129
130InductionVarRange::Value InductionVarRange::GetFetch(HInstruction* instruction,
Aart Bik22af3be2015-09-10 12:50:58 -0700131 HInductionVarAnalysis::InductionInfo* trip,
Aart Bikb3365e02015-09-21 14:45:05 -0700132 bool is_min) {
Aart Bikd14c5952015-09-08 15:25:15 -0700133 // Detect constants and chase the fetch a bit deeper into the HIR tree, so that it becomes
134 // more likely range analysis will compare the same instructions as terminal nodes.
135 int32_t value;
136 if (IsIntAndGet(instruction, &value)) {
137 return Value(value);
138 } else if (instruction->IsAdd()) {
139 if (IsIntAndGet(instruction->InputAt(0), &value)) {
Aart Bikb3365e02015-09-21 14:45:05 -0700140 return AddValue(Value(value), GetFetch(instruction->InputAt(1), trip, is_min));
Aart Bikd14c5952015-09-08 15:25:15 -0700141 } else if (IsIntAndGet(instruction->InputAt(1), &value)) {
Aart Bikb3365e02015-09-21 14:45:05 -0700142 return AddValue(GetFetch(instruction->InputAt(0), trip, is_min), Value(value));
Aart Bik22af3be2015-09-10 12:50:58 -0700143 }
Aart Bikb3365e02015-09-21 14:45:05 -0700144 } else if (is_min) {
145 // Special case for finding minimum: minimum of trip-count is 1.
Aart Bik22af3be2015-09-10 12:50:58 -0700146 if (trip != nullptr && instruction == trip->op_b->fetch) {
147 return Value(1);
Aart Bikd14c5952015-09-08 15:25:15 -0700148 }
149 }
150 return Value(instruction, 1, 0);
151}
152
153InductionVarRange::Value InductionVarRange::GetMin(HInductionVarAnalysis::InductionInfo* info,
154 HInductionVarAnalysis::InductionInfo* trip) {
155 if (info != nullptr) {
156 switch (info->induction_class) {
157 case HInductionVarAnalysis::kInvariant:
158 // Invariants.
159 switch (info->operation) {
160 case HInductionVarAnalysis::kNop: // normalized: 0
161 DCHECK_EQ(info->op_a, info->op_b);
162 return Value(0);
163 case HInductionVarAnalysis::kAdd:
Aart Bikb3365e02015-09-21 14:45:05 -0700164 return AddValue(GetMin(info->op_a, trip), GetMin(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700165 case HInductionVarAnalysis::kSub: // second max!
Aart Bikb3365e02015-09-21 14:45:05 -0700166 return SubValue(GetMin(info->op_a, trip), GetMax(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700167 case HInductionVarAnalysis::kNeg: // second max!
Aart Bikb3365e02015-09-21 14:45:05 -0700168 return SubValue(Value(0), GetMax(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700169 case HInductionVarAnalysis::kMul:
Aart Bikb3365e02015-09-21 14:45:05 -0700170 return GetMul(info->op_a, info->op_b, trip, true);
Aart Bikd14c5952015-09-08 15:25:15 -0700171 case HInductionVarAnalysis::kDiv:
Aart Bikb3365e02015-09-21 14:45:05 -0700172 return GetDiv(info->op_a, info->op_b, trip, true);
Aart Bikd14c5952015-09-08 15:25:15 -0700173 case HInductionVarAnalysis::kFetch:
Aart Bikb3365e02015-09-21 14:45:05 -0700174 return GetFetch(info->fetch, trip, true);
Aart Bikd14c5952015-09-08 15:25:15 -0700175 }
176 break;
177 case HInductionVarAnalysis::kLinear:
178 // Minimum over linear induction a * i + b, for normalized 0 <= i < TC.
Aart Bikb3365e02015-09-21 14:45:05 -0700179 return AddValue(GetMul(info->op_a, trip, trip, true), GetMin(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700180 case HInductionVarAnalysis::kWrapAround:
181 case HInductionVarAnalysis::kPeriodic:
182 // Minimum over all values in the wrap-around/periodic.
183 return MinValue(GetMin(info->op_a, trip), GetMin(info->op_b, trip));
184 }
185 }
Aart Bikb3365e02015-09-21 14:45:05 -0700186 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700187}
188
189InductionVarRange::Value InductionVarRange::GetMax(HInductionVarAnalysis::InductionInfo* info,
190 HInductionVarAnalysis::InductionInfo* trip) {
191 if (info != nullptr) {
192 switch (info->induction_class) {
193 case HInductionVarAnalysis::kInvariant:
194 // Invariants.
195 switch (info->operation) {
196 case HInductionVarAnalysis::kNop: // normalized: TC - 1
197 DCHECK_EQ(info->op_a, info->op_b);
Aart Bikb3365e02015-09-21 14:45:05 -0700198 return SubValue(GetMax(info->op_b, trip), Value(1));
Aart Bikd14c5952015-09-08 15:25:15 -0700199 case HInductionVarAnalysis::kAdd:
Aart Bikb3365e02015-09-21 14:45:05 -0700200 return AddValue(GetMax(info->op_a, trip), GetMax(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700201 case HInductionVarAnalysis::kSub: // second min!
Aart Bikb3365e02015-09-21 14:45:05 -0700202 return SubValue(GetMax(info->op_a, trip), GetMin(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700203 case HInductionVarAnalysis::kNeg: // second min!
Aart Bikb3365e02015-09-21 14:45:05 -0700204 return SubValue(Value(0), GetMin(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700205 case HInductionVarAnalysis::kMul:
Aart Bikb3365e02015-09-21 14:45:05 -0700206 return GetMul(info->op_a, info->op_b, trip, false);
Aart Bikd14c5952015-09-08 15:25:15 -0700207 case HInductionVarAnalysis::kDiv:
Aart Bikb3365e02015-09-21 14:45:05 -0700208 return GetDiv(info->op_a, info->op_b, trip, false);
Aart Bikd14c5952015-09-08 15:25:15 -0700209 case HInductionVarAnalysis::kFetch:
Aart Bikb3365e02015-09-21 14:45:05 -0700210 return GetFetch(info->fetch, trip, false);
Aart Bikd14c5952015-09-08 15:25:15 -0700211 }
212 break;
213 case HInductionVarAnalysis::kLinear:
214 // Maximum over linear induction a * i + b, for normalized 0 <= i < TC.
Aart Bikb3365e02015-09-21 14:45:05 -0700215 return AddValue(GetMul(info->op_a, trip, trip, false), GetMax(info->op_b, trip));
Aart Bikd14c5952015-09-08 15:25:15 -0700216 case HInductionVarAnalysis::kWrapAround:
217 case HInductionVarAnalysis::kPeriodic:
218 // Maximum over all values in the wrap-around/periodic.
219 return MaxValue(GetMax(info->op_a, trip), GetMax(info->op_b, trip));
220 }
221 }
Aart Bikb3365e02015-09-21 14:45:05 -0700222 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700223}
224
225InductionVarRange::Value InductionVarRange::GetMul(HInductionVarAnalysis::InductionInfo* info1,
226 HInductionVarAnalysis::InductionInfo* info2,
227 HInductionVarAnalysis::InductionInfo* trip,
Aart Bikb3365e02015-09-21 14:45:05 -0700228 bool is_min) {
Aart Bikd14c5952015-09-08 15:25:15 -0700229 Value v1_min = GetMin(info1, trip);
230 Value v1_max = GetMax(info1, trip);
231 Value v2_min = GetMin(info2, trip);
232 Value v2_max = GetMax(info2, trip);
Aart Bikb3365e02015-09-21 14:45:05 -0700233 if (v1_min.is_known && v1_min.a_constant == 0 && v1_min.b_constant >= 0) {
Aart Bikd14c5952015-09-08 15:25:15 -0700234 // Positive range vs. positive or negative range.
Aart Bikb3365e02015-09-21 14:45:05 -0700235 if (v2_min.is_known && v2_min.a_constant == 0 && v2_min.b_constant >= 0) {
236 return is_min ? MulValue(v1_min, v2_min)
237 : MulValue(v1_max, v2_max);
238 } else if (v2_max.is_known && v2_max.a_constant == 0 && v2_max.b_constant <= 0) {
239 return is_min ? MulValue(v1_max, v2_min)
240 : MulValue(v1_min, v2_max);
Aart Bikd14c5952015-09-08 15:25:15 -0700241 }
Aart Bikb3365e02015-09-21 14:45:05 -0700242 } else if (v1_min.is_known && v1_min.a_constant == 0 && v1_min.b_constant <= 0) {
Aart Bikd14c5952015-09-08 15:25:15 -0700243 // Negative range vs. positive or negative range.
Aart Bikb3365e02015-09-21 14:45:05 -0700244 if (v2_min.is_known && v2_min.a_constant == 0 && v2_min.b_constant >= 0) {
245 return is_min ? MulValue(v1_min, v2_max)
246 : MulValue(v1_max, v2_min);
247 } else if (v2_max.is_known && v2_max.a_constant == 0 && v2_max.b_constant <= 0) {
248 return is_min ? MulValue(v1_max, v2_max)
249 : MulValue(v1_min, v2_min);
Aart Bikd14c5952015-09-08 15:25:15 -0700250 }
251 }
Aart Bikb3365e02015-09-21 14:45:05 -0700252 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700253}
254
255InductionVarRange::Value InductionVarRange::GetDiv(HInductionVarAnalysis::InductionInfo* info1,
256 HInductionVarAnalysis::InductionInfo* info2,
257 HInductionVarAnalysis::InductionInfo* trip,
Aart Bikb3365e02015-09-21 14:45:05 -0700258 bool is_min) {
Aart Bikd14c5952015-09-08 15:25:15 -0700259 Value v1_min = GetMin(info1, trip);
260 Value v1_max = GetMax(info1, trip);
261 Value v2_min = GetMin(info2, trip);
262 Value v2_max = GetMax(info2, trip);
Aart Bikb3365e02015-09-21 14:45:05 -0700263 if (v1_min.is_known && v1_min.a_constant == 0 && v1_min.b_constant >= 0) {
Aart Bikd14c5952015-09-08 15:25:15 -0700264 // Positive range vs. positive or negative range.
Aart Bikb3365e02015-09-21 14:45:05 -0700265 if (v2_min.is_known && v2_min.a_constant == 0 && v2_min.b_constant >= 0) {
266 return is_min ? DivValue(v1_min, v2_max)
267 : DivValue(v1_max, v2_min);
268 } else if (v2_max.is_known && v2_max.a_constant == 0 && v2_max.b_constant <= 0) {
269 return is_min ? DivValue(v1_max, v2_max)
270 : DivValue(v1_min, v2_min);
Aart Bikd14c5952015-09-08 15:25:15 -0700271 }
Aart Bikb3365e02015-09-21 14:45:05 -0700272 } else if (v1_min.is_known && v1_min.a_constant == 0 && v1_min.b_constant <= 0) {
Aart Bikd14c5952015-09-08 15:25:15 -0700273 // Negative range vs. positive or negative range.
Aart Bikb3365e02015-09-21 14:45:05 -0700274 if (v2_min.is_known && v2_min.a_constant == 0 && v2_min.b_constant >= 0) {
275 return is_min ? DivValue(v1_min, v2_min)
276 : DivValue(v1_max, v2_max);
277 } else if (v2_max.is_known && v2_max.a_constant == 0 && v2_max.b_constant <= 0) {
278 return is_min ? DivValue(v1_max, v2_min)
279 : DivValue(v1_min, v2_max);
Aart Bikd14c5952015-09-08 15:25:15 -0700280 }
281 }
Aart Bikb3365e02015-09-21 14:45:05 -0700282 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700283}
284
Aart Bikb3365e02015-09-21 14:45:05 -0700285InductionVarRange::Value InductionVarRange::AddValue(Value v1, Value v2) {
286 if (v1.is_known && v2.is_known && IsSafeAdd(v1.b_constant, v2.b_constant)) {
Aart Bikd14c5952015-09-08 15:25:15 -0700287 const int32_t b = v1.b_constant + v2.b_constant;
288 if (v1.a_constant == 0) {
289 return Value(v2.instruction, v2.a_constant, b);
290 } else if (v2.a_constant == 0) {
291 return Value(v1.instruction, v1.a_constant, b);
292 } else if (v1.instruction == v2.instruction && IsSafeAdd(v1.a_constant, v2.a_constant)) {
293 return Value(v1.instruction, v1.a_constant + v2.a_constant, b);
294 }
295 }
Aart Bikb3365e02015-09-21 14:45:05 -0700296 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700297}
298
Aart Bikb3365e02015-09-21 14:45:05 -0700299InductionVarRange::Value InductionVarRange::SubValue(Value v1, Value v2) {
300 if (v1.is_known && v2.is_known && IsSafeSub(v1.b_constant, v2.b_constant)) {
Aart Bikd14c5952015-09-08 15:25:15 -0700301 const int32_t b = v1.b_constant - v2.b_constant;
302 if (v1.a_constant == 0 && IsSafeSub(0, v2.a_constant)) {
303 return Value(v2.instruction, -v2.a_constant, b);
304 } else if (v2.a_constant == 0) {
305 return Value(v1.instruction, v1.a_constant, b);
306 } else if (v1.instruction == v2.instruction && IsSafeSub(v1.a_constant, v2.a_constant)) {
307 return Value(v1.instruction, v1.a_constant - v2.a_constant, b);
308 }
309 }
Aart Bikb3365e02015-09-21 14:45:05 -0700310 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700311}
312
Aart Bikb3365e02015-09-21 14:45:05 -0700313InductionVarRange::Value InductionVarRange::MulValue(Value v1, Value v2) {
314 if (v1.is_known && v2.is_known) {
315 if (v1.a_constant == 0) {
316 if (IsSafeMul(v1.b_constant, v2.a_constant) && IsSafeMul(v1.b_constant, v2.b_constant)) {
317 return Value(v2.instruction, v1.b_constant * v2.a_constant, v1.b_constant * v2.b_constant);
318 }
319 } else if (v2.a_constant == 0) {
320 if (IsSafeMul(v1.a_constant, v2.b_constant) && IsSafeMul(v1.b_constant, v2.b_constant)) {
321 return Value(v1.instruction, v1.a_constant * v2.b_constant, v1.b_constant * v2.b_constant);
322 }
Aart Bikd14c5952015-09-08 15:25:15 -0700323 }
324 }
Aart Bikb3365e02015-09-21 14:45:05 -0700325 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700326}
327
Aart Bikb3365e02015-09-21 14:45:05 -0700328InductionVarRange::Value InductionVarRange::DivValue(Value v1, Value v2) {
329 if (v1.is_known && v2.is_known && v1.a_constant == 0 && v2.a_constant == 0) {
Aart Bikd14c5952015-09-08 15:25:15 -0700330 if (IsSafeDiv(v1.b_constant, v2.b_constant)) {
331 return Value(v1.b_constant / v2.b_constant);
332 }
333 }
Aart Bikb3365e02015-09-21 14:45:05 -0700334 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700335}
336
337InductionVarRange::Value InductionVarRange::MinValue(Value v1, Value v2) {
Aart Bikb3365e02015-09-21 14:45:05 -0700338 if (v1.is_known && v2.is_known) {
339 if (v1.instruction == v2.instruction && v1.a_constant == v2.a_constant) {
340 return Value(v1.instruction, v1.a_constant, std::min(v1.b_constant, v2.b_constant));
341 }
Aart Bikd14c5952015-09-08 15:25:15 -0700342 }
Aart Bikb3365e02015-09-21 14:45:05 -0700343 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700344}
345
346InductionVarRange::Value InductionVarRange::MaxValue(Value v1, Value v2) {
Aart Bikb3365e02015-09-21 14:45:05 -0700347 if (v1.is_known && v2.is_known) {
348 if (v1.instruction == v2.instruction && v1.a_constant == v2.a_constant) {
349 return Value(v1.instruction, v1.a_constant, std::max(v1.b_constant, v2.b_constant));
350 }
Aart Bikd14c5952015-09-08 15:25:15 -0700351 }
Aart Bikb3365e02015-09-21 14:45:05 -0700352 return Value();
Aart Bikd14c5952015-09-08 15:25:15 -0700353}
354
355} // namespace art