Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(129)

Side by Side Diff: src/hydrogen-instructions.cc

Issue 17568015: New array bounds check elimination pass (focused on induction variables and bitwise operations). (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Recursive traversal implementation. Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright 2012 the V8 project authors. All rights reserved. 1 // Copyright 2012 the V8 project authors. All rights reserved.
2 // Redistribution and use in source and binary forms, with or without 2 // Redistribution and use in source and binary forms, with or without
3 // modification, are permitted provided that the following conditions are 3 // modification, are permitted provided that the following conditions are
4 // met: 4 // met:
5 // 5 //
6 // * Redistributions of source code must retain the above copyright 6 // * Redistributions of source code must retain the above copyright
7 // notice, this list of conditions and the following disclaimer. 7 // notice, this list of conditions and the following disclaimer.
8 // * Redistributions in binary form must reproduce the above 8 // * Redistributions in binary form must reproduce the above
9 // copyright notice, this list of conditions and the following 9 // copyright notice, this list of conditions and the following
10 // disclaimer in the documentation and/or other materials provided 10 // disclaimer in the documentation and/or other materials provided
(...skipping 1015 matching lines...) Expand 10 before | Expand all | Expand 10 after
1026 if (context->upper_bound() == length() && 1026 if (context->upper_bound() == length() &&
1027 context->lower_bound_guarantee() != NULL && 1027 context->lower_bound_guarantee() != NULL &&
1028 context->lower_bound_guarantee() != this && 1028 context->lower_bound_guarantee() != this &&
1029 context->lower_bound_guarantee()->block() != block() && 1029 context->lower_bound_guarantee()->block() != block() &&
1030 offset() < context->offset() && 1030 offset() < context->offset() &&
1031 index_can_increase() && 1031 index_can_increase() &&
1032 context->upper_bound_guarantee() == NULL) { 1032 context->upper_bound_guarantee() == NULL) {
1033 offset_ = context->offset(); 1033 offset_ = context->offset();
1034 SetResponsibilityForRange(DIRECTION_UPPER); 1034 SetResponsibilityForRange(DIRECTION_UPPER);
1035 context->set_upper_bound_guarantee(this); 1035 context->set_upper_bound_guarantee(this);
1036 isolate()->counters()->bounds_checks_covered()->Increment();
1036 } else if (context->upper_bound_guarantee() != NULL && 1037 } else if (context->upper_bound_guarantee() != NULL &&
1037 context->upper_bound_guarantee() != this && 1038 context->upper_bound_guarantee() != this &&
1038 context->upper_bound_guarantee()->block() != block() && 1039 context->upper_bound_guarantee()->block() != block() &&
1039 offset() > context->offset() && 1040 offset() > context->offset() &&
1040 index_can_decrease() && 1041 index_can_decrease() &&
1041 context->lower_bound_guarantee() == NULL) { 1042 context->lower_bound_guarantee() == NULL) {
1042 offset_ = context->offset(); 1043 offset_ = context->offset();
1043 SetResponsibilityForRange(DIRECTION_LOWER); 1044 SetResponsibilityForRange(DIRECTION_LOWER);
1044 context->set_lower_bound_guarantee(this); 1045 context->set_lower_bound_guarantee(this);
1046 isolate()->counters()->bounds_checks_covered()->Increment();
1045 } 1047 }
1046 } 1048 }
1047 1049
1048 1050
1049 void HBoundsCheck::ApplyIndexChange() { 1051 void HBoundsCheck::ApplyIndexChange() {
1050 if (skip_check()) return; 1052 if (skip_check()) return;
1051 1053
1052 DecompositionResult decomposition; 1054 DecompositionResult decomposition;
1053 bool index_is_decomposable = index()->TryDecompose(&decomposition); 1055 bool index_is_decomposable = index()->TryDecompose(&decomposition);
1054 if (index_is_decomposable) { 1056 if (index_is_decomposable) {
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
1095 scale_ = 0; 1097 scale_ = 0;
1096 responsibility_direction_ = DIRECTION_NONE; 1098 responsibility_direction_ = DIRECTION_NONE;
1097 } 1099 }
1098 1100
1099 1101
1100 void HBoundsCheck::AddInformativeDefinitions() { 1102 void HBoundsCheck::AddInformativeDefinitions() {
1101 // TODO(mmassi): Executing this code during AddInformativeDefinitions 1103 // TODO(mmassi): Executing this code during AddInformativeDefinitions
1102 // is a hack. Move it to some other HPhase. 1104 // is a hack. Move it to some other HPhase.
1103 if (FLAG_array_bounds_checks_elimination) { 1105 if (FLAG_array_bounds_checks_elimination) {
1104 if (index()->TryGuaranteeRange(length())) { 1106 if (index()->TryGuaranteeRange(length())) {
1105 set_skip_check(true); 1107 set_skip_check();
1106 } 1108 }
1107 if (DetectCompoundIndex()) { 1109 if (DetectCompoundIndex()) {
1108 HBoundsCheckBaseIndexInformation* base_index_info = 1110 HBoundsCheckBaseIndexInformation* base_index_info =
1109 new(block()->graph()->zone()) 1111 new(block()->graph()->zone())
1110 HBoundsCheckBaseIndexInformation(this); 1112 HBoundsCheckBaseIndexInformation(this);
1111 base_index_info->InsertAfter(this); 1113 base_index_info->InsertAfter(this);
1112 } 1114 }
1113 } 1115 }
1114 } 1116 }
1115 1117
(...skipping 825 matching lines...) Expand 10 before | Expand all | Expand 10 after
1941 result = false; 1943 result = false;
1942 break; 1944 break;
1943 } 1945 }
1944 } 1946 }
1945 ClearFlag(kNumericConstraintEvaluationInProgress); 1947 ClearFlag(kNumericConstraintEvaluationInProgress);
1946 1948
1947 return result; 1949 return result;
1948 } 1950 }
1949 1951
1950 1952
1953 InductionVariableData* InductionVariableData::ExaminePhi(HPhi* phi) {
1954 if (phi->block()->loop_information() == NULL) return NULL;
1955 if (phi->OperandCount() != 2) return NULL;
1956 int32_t candidate_increment;
1957
1958 candidate_increment = ComputeIncrement(phi, phi->OperandAt(0));
1959 if (candidate_increment != 0) {
1960 return new(phi->block()->graph()->zone())
1961 InductionVariableData(phi, phi->OperandAt(1), candidate_increment);
1962 }
1963
1964 candidate_increment = ComputeIncrement(phi, phi->OperandAt(1));
1965 if (candidate_increment != 0) {
1966 return new(phi->block()->graph()->zone())
1967 InductionVariableData(phi, phi->OperandAt(0), candidate_increment);
1968 }
1969
1970 return NULL;
1971 }
1972
1973 /*
1974 * This function tries to match the following patterns (and all the relevant
1975 * variants related to |, & and + being commutative):
1976 * base | constant_or_mask
1977 * base & constant_and_mask
1978 * (base + constant_offset) & constant_and_mask
1979 * (base - constant_offset) & constant_and_mask
titzer 2013/07/18 12:22:58 I don't understand why we don't match (base + cons
Massi 2013/07/23 07:57:50 Because the old bounds check elimination phase alr
1980 */
1981 void InductionVariableData::DecomposeBitwise(
1982 HValue* value,
1983 BitwiseDecompositionResult* result) {
1984 HValue* base = IgnoreOsrValue(value);
1985 result->base = value;
1986 int32_t* mask_location = NULL;
1987
1988 if (!base->representation().IsInteger32()) return;
1989
1990 if (base->IsBitwise()) {
1991 bool allow_offset = false;
1992
1993 HBitwise* bitwise = HBitwise::cast(base);
1994 if (bitwise->op() == Token::BIT_AND) {
1995 mask_location = &(result->and_mask);
1996 allow_offset = true;
1997 } else if (bitwise->op() == Token::BIT_OR) {
1998 mask_location = &(result->or_mask);
1999 } else {
2000 return;
2001 }
2002 if (bitwise->right()->IsInteger32Constant()) {
titzer 2013/07/18 12:22:58 As per previous comments, please don't use a point
Massi 2013/07/23 07:57:50 Done.
2003 *mask_location = bitwise->right()->GetInteger32Constant();
2004 base = bitwise->left();
2005 } else if (bitwise->left()->IsInteger32Constant()) {
2006 *mask_location = bitwise->left()->GetInteger32Constant();
2007 base = bitwise->right();
2008 } else {
2009 return;
2010 }
2011
2012 result->context = bitwise->context();
2013
2014 if (allow_offset) {
2015 if (base->IsAdd()) {
2016 HAdd* add = HAdd::cast(base);
2017 if (add->right()->IsInteger32Constant()) {
2018 base = add->left();
2019 } else if (add->left()->IsInteger32Constant()) {
2020 base = add->right();
2021 }
2022 } else if (base->IsSub()) {
2023 HSub* sub = HSub::cast(base);
2024 if (sub->right()->IsInteger32Constant()) {
2025 base = sub->left();
2026 }
2027 }
2028 }
2029
2030 result->base = base;
2031 }
2032 }
2033
2034
2035 void InductionVariableData::AddCheck(HBoundsCheck* check,
2036 int32_t upper_limit) {
2037 ASSERT(limit_validity() != NULL);
2038 if (limit_validity() != check->block() &&
2039 !limit_validity()->Dominates(check->block())) return;
2040 if (!phi()->block()->current_loop()->IsNestedInThisLoop(
2041 check->block()->current_loop())) return;
2042
2043 ChecksRelatedToLength* length_checks = checks();
2044 while (length_checks != NULL) {
2045 if (length_checks->length() == check->length()) break;
2046 length_checks = length_checks->next();
2047 }
2048 if (length_checks == NULL) {
2049 length_checks = new(check->block()->zone())
2050 ChecksRelatedToLength(check->length(), checks());
2051 checks_ = length_checks;
2052 }
2053
2054 length_checks->AddCheck(check, upper_limit);
2055 }
2056
2057 void InductionVariableData::ChecksRelatedToLength::CloseCurrentBlock() {
2058 if (checks() != NULL) {
2059 InductionVariableCheck* c = checks();
2060 HBasicBlock* current_block = c->check()->block();
2061 while (c != NULL && c->check()->block() == current_block) {
2062 c->set_upper_limit(current_upper_limit_);
2063 c = c->next();
2064 }
2065 }
2066 }
2067
2068
2069 void InductionVariableData::ChecksRelatedToLength::UseNewIndexInCurrentBlock(
2070 Token::Value token,
2071 int32_t mask,
2072 HValue* index_base,
2073 HValue* context) {
2074 ASSERT(first_check_in_block() != NULL);
2075 HValue* previous_index = first_check_in_block()->index();
2076 ASSERT(context != NULL);
2077
2078 set_added_constant(new(index_base->block()->graph()->zone()) HConstant(
2079 mask, index_base->representation()));
2080 if (added_index() != NULL) {
2081 added_constant()->InsertBefore(added_index());
2082 } else {
2083 added_constant()->InsertBefore(first_check_in_block());
2084 }
2085
2086 if (added_index() == NULL) {
2087 first_check_in_block()->ReplaceAllUsesWith(first_check_in_block()->index());
2088 HInstruction* new_index = HBitwise::New(
2089 index_base->block()->graph()->zone(),
2090 token, context, index_base, added_constant());
2091 ASSERT(new_index->IsBitwise());
2092 new_index->ClearAllSideEffects();
2093 new_index->AssumeRepresentation(Representation::Integer32());
2094 set_added_index(HBitwise::cast(new_index));
2095 added_index()->InsertBefore(first_check_in_block());
2096 }
2097 ASSERT(added_index()->op() == token);
2098
2099 added_index()->SetOperandAt(1, index_base);
2100 added_index()->SetOperandAt(2, added_constant());
2101 first_check_in_block()->SetOperandAt(0, added_index());
2102 if (previous_index->UseCount() == 0) {
2103 previous_index->DeleteAndReplaceWith(NULL);
2104 }
2105 }
2106
2107 void InductionVariableData::ChecksRelatedToLength::AddCheck(
2108 HBoundsCheck* check,
2109 int32_t upper_limit) {
2110 BitwiseDecompositionResult decomposition;
2111 InductionVariableData::DecomposeBitwise(check->index(), &decomposition);
2112
2113 if (first_check_in_block() == NULL ||
2114 first_check_in_block()->block() != check->block()) {
2115 CloseCurrentBlock();
2116
2117 first_check_in_block_ = check;
2118 set_added_index(NULL);
2119 set_added_constant(NULL);
2120 current_and_mask_in_block_ = decomposition.and_mask;
2121 current_or_mask_in_block_ = decomposition.or_mask;
2122 current_upper_limit_ = upper_limit;
2123
2124 InductionVariableCheck* new_check = new(check->block()->graph()->zone())
2125 InductionVariableCheck(check, checks_, upper_limit);
2126 checks_ = new_check;
2127 return;
2128 }
2129
2130 if (upper_limit > current_upper_limit()) {
2131 current_upper_limit_ = upper_limit;
2132 }
2133
2134 if (decomposition.and_mask != 0 &&
2135 current_or_mask_in_block() == 0) {
2136 if (current_and_mask_in_block() == 0 ||
2137 decomposition.and_mask > current_and_mask_in_block()) {
2138 UseNewIndexInCurrentBlock(Token::BIT_AND,
2139 decomposition.and_mask,
2140 decomposition.base,
2141 decomposition.context);
2142 current_and_mask_in_block_ = decomposition.and_mask;
2143 }
2144 check->set_skip_check();
2145 }
2146 if (current_and_mask_in_block() == 0) {
2147 if (decomposition.or_mask > current_or_mask_in_block()) {
2148 UseNewIndexInCurrentBlock(Token::BIT_OR,
2149 decomposition.or_mask,
2150 decomposition.base,
2151 decomposition.context);
2152 current_or_mask_in_block_ = decomposition.or_mask;
2153 }
2154 check->set_skip_check();
2155 }
2156
2157 if (!check->skip_check()) {
2158 InductionVariableCheck* new_check = new(check->block()->graph()->zone())
2159 InductionVariableCheck(check, checks_, upper_limit);
2160 checks_ = new_check;
2161 }
2162 }
2163
2164
2165 /*
2166 * This method detects if phi is an induction variable, with phi_operand as
2167 * its "incremented" value (the other operand would be the "base" value).
2168 *
2169 * It cheks is phi_operand has the form "phi + constant".
2170 * If yes, the constant is the increment that the induction variable gets at
2171 * every loop iteration.
2172 * Otherwise it returns 0.
2173 */
2174 int32_t InductionVariableData::ComputeIncrement(HPhi* phi,
2175 HValue* phi_operand) {
2176 if (!phi_operand->representation().IsInteger32()) return 0;
2177
2178 if (phi_operand->IsAdd()) {
2179 HAdd* operation = HAdd::cast(phi_operand);
2180 if (operation->left() == phi &&
2181 operation->right()->IsInteger32Constant()) {
2182 return operation->right()->GetInteger32Constant();
2183 } else if (operation->right() == phi &&
2184 operation->left()->IsInteger32Constant()) {
2185 return operation->left()->GetInteger32Constant();
2186 }
2187 } else if (phi_operand->IsSub()) {
2188 HSub* operation = HSub::cast(phi_operand);
2189 if (operation->left() == phi &&
2190 operation->right()->IsInteger32Constant()) {
2191 return -operation->right()->GetInteger32Constant();
2192 }
2193 }
2194
2195 return 0;
2196 }
2197
2198
2199 /*
2200 * Swaps the information in "update" with the one contained in "this".
2201 * The swapping is important because this method is used while doing a
2202 * dominator tree traversal, and "update" will retain the old data that
2203 * will be restored while backtracking.
2204 */
2205 void InductionVariableData::UpdateAdditionalLimit(
2206 InductionVariableLimitUpdate* update) {
2207 ASSERT(update->updated_variable == this);
2208 if (update->limit_is_upper) {
2209 swap(&additional_upper_limit_, &update->limit);
2210 swap(&additional_upper_limit_is_included_, &update->limit_is_included);
2211 } else {
2212 swap(&additional_lower_limit_, &update->limit);
2213 swap(&additional_lower_limit_is_included_, &update->limit_is_included);
2214 }
2215 }
2216
2217
2218 int32_t InductionVariableData::ComputeUpperLimit(int32_t and_mask,
2219 int32_t or_mask) {
2220 // Should be Smi::kMaxValue but it must fit 32 bits; lower is safe anyway.
2221 const int32_t MAX_LIMIT = 1 << 30;
2222
2223 int32_t result = MAX_LIMIT;
2224
2225 if (limit() != NULL &&
2226 limit()->IsInteger32Constant()) {
2227 int32_t limit_value = limit()->GetInteger32Constant();
2228 if (!limit_included()) {
2229 limit_value--;
2230 }
2231 if (limit_value < result) result = limit_value;
2232 }
2233
2234 if (additional_upper_limit() != NULL &&
2235 additional_upper_limit()->IsInteger32Constant()) {
2236 int32_t limit_value = additional_upper_limit()->GetInteger32Constant();
2237 if (!additional_upper_limit_is_included()) {
2238 limit_value--;
2239 }
2240 if (limit_value < result) result = limit_value;
2241 }
2242
2243 if (and_mask > 0 && and_mask < MAX_LIMIT) {
2244 if (and_mask < result) result = and_mask;
2245 return result;
2246 }
2247
2248 // Add the effect of the or_mask.
2249 result |= or_mask;
2250
2251 if (result >= MAX_LIMIT) {
titzer 2013/07/18 12:22:58 Can use a ternary here.
Massi 2013/07/23 07:57:50 Done.
2252 result = kNoLimit;
2253 }
2254 return result;
2255 }
2256
2257
2258 HValue* InductionVariableData::IgnoreOsrValue(HValue* v) {
2259 if (!v->IsPhi()) return v;
2260 HPhi* phi = HPhi::cast(v);
2261 if (phi->OperandCount() != 2) return v;
2262 if (phi->OperandAt(0)->block()->is_osr_entry()) {
2263 return phi->OperandAt(1);
2264 } else if (phi->OperandAt(1)->block()->is_osr_entry()) {
2265 return phi->OperandAt(0);
2266 } else {
2267 return v;
2268 }
2269 }
2270
2271
2272 InductionVariableData* InductionVariableData::GetInductionVariableData(
2273 HValue* v) {
2274 v = IgnoreOsrValue(v);
2275 if (v->IsPhi()) {
2276 return HPhi::cast(v)->induction_variable_data();
2277 }
2278 return NULL;
2279 }
2280
2281
2282 /*
2283 * Check if a conditional branch to "current_branch" with token "token" is
2284 * the branch that keeps the induction loop running (and, conversely, will
2285 * terminate it if the "other_branch" is taken).
2286 *
2287 * Three conditions must be met:
2288 * - "current_branch" must be in the induction loop.
2289 * - "other_branch" must be out of the induction loop.
2290 * - "token" and the induction increment must be "compatible": the token should
2291 * be a condition that keeps the execution inside the loop until the limit is
2292 * reached.
2293 */
2294 bool InductionVariableData::CheckIfBranchIsLoopGuard(
2295 Token::Value token,
2296 HBasicBlock* current_branch,
2297 HBasicBlock* other_branch) {
2298 if (!phi()->block()->current_loop()->IsNestedInThisLoop(
2299 current_branch->current_loop())) {
2300 return false;
2301 }
2302
2303 if (phi()->block()->current_loop()->IsNestedInThisLoop(
2304 other_branch->current_loop())) {
2305 return false;
2306 }
2307
2308 if (increment() > 0 && (token == Token::LT || token == Token::LTE)) {
2309 return true;
2310 }
2311 if (increment() < 0 && (token == Token::GT || token == Token::GTE)) {
2312 return true;
2313 }
2314 if (Token::IsInequalityOp(token) && (increment() == 1 || increment() == -1)) {
2315 return true;
2316 }
2317
2318 return false;
2319 }
2320
2321
2322 void InductionVariableData::ComputeLimitFromPredecessorBlock(
2323 HBasicBlock* block,
2324 LimitFromPredecessorBlock* result) {
2325 if (block->predecessors()->length() != 1) return;
2326 HBasicBlock* predecessor = block->predecessors()->at(0);
2327 HInstruction* end = predecessor->last();
2328
2329 if (!end->IsCompareIDAndBranch()) return;
2330 HCompareIDAndBranch* branch = HCompareIDAndBranch::cast(end);
2331
2332 Token::Value token = branch->token();
2333 if (!Token::IsArithmeticCompareOp(token)) return;
2334
2335 HBasicBlock* other_target;
2336 if (block == branch->SuccessorAt(0)) {
2337 other_target = branch->SuccessorAt(1);
2338 } else {
2339 other_target = branch->SuccessorAt(0);
2340 token = Token::NegateCompareOp(token);
2341 ASSERT(block == branch->SuccessorAt(1));
2342 }
2343
2344 InductionVariableData* data;
2345
2346 data = GetInductionVariableData(branch->left());
2347 HValue* limit = branch->right();
2348 if (data == NULL) {
2349 data = GetInductionVariableData(branch->right());
2350 token = Token::ReverseCompareOp(token);
2351 limit = branch->left();
2352 }
2353
2354 if (data != NULL) {
2355 result->variable = data;
2356 result->token = token;
2357 result->limit = limit;
2358 result->other_target = other_target;
2359 }
2360 }
2361
2362 /*
2363 * Compute the limit that is imposed on an induction variable when entering
2364 * "block" (if any).
2365 * If the limit is the "proper" induction limit (the one that makes the loop
2366 * terminate when the induction variable reaches it) it is stored directly in
2367 * the induction variable data.
2368 * Otherwise the limit is written in "additional_limit" and the method
2369 * returns true.
2370 */
2371 bool InductionVariableData::ComputeInductionVariableLimit(
2372 HBasicBlock* block,
2373 InductionVariableLimitUpdate* additional_limit) {
2374 LimitFromPredecessorBlock limit;
2375 ComputeLimitFromPredecessorBlock(block, &limit);
2376 if (!limit.LimitIsValid()) return false;
2377
2378 if (limit.variable->CheckIfBranchIsLoopGuard(limit.token,
2379 block,
2380 limit.other_target)) {
2381 limit.variable->limit_ = limit.limit;
2382 limit.variable->limit_included_ = limit.LimitIsIncluded();
2383 limit.variable->limit_validity_ = block;
2384 limit.variable->induction_exit_block_ = block->predecessors()->at(0);
2385 limit.variable->induction_exit_target_ = limit.other_target;
2386 return false;
2387 } else {
2388 additional_limit->updated_variable = limit.variable;
2389 additional_limit->limit = limit.limit;
2390 additional_limit->limit_is_upper = limit.LimitIsUpper();
2391 additional_limit->limit_is_included = limit.LimitIsIncluded();
2392 return true;
2393 }
2394 }
2395
2396
1951 Range* HMathMinMax::InferRange(Zone* zone) { 2397 Range* HMathMinMax::InferRange(Zone* zone) {
1952 if (representation().IsInteger32()) { 2398 if (representation().IsInteger32()) {
1953 Range* a = left()->range(); 2399 Range* a = left()->range();
1954 Range* b = right()->range(); 2400 Range* b = right()->range();
1955 Range* res = a->Copy(zone); 2401 Range* res = a->Copy(zone);
1956 if (operation_ == kMathMax) { 2402 if (operation_ == kMathMax) {
1957 res->CombinedMax(b); 2403 res->CombinedMax(b);
1958 } else { 2404 } else {
1959 ASSERT(operation_ == kMathMin); 2405 ASSERT(operation_ == kMathMin);
1960 res->CombinedMin(b); 2406 res->CombinedMin(b);
(...skipping 1905 matching lines...) Expand 10 before | Expand all | Expand 10 after
3866 case kBackingStore: 4312 case kBackingStore:
3867 if (!name_.is_null()) stream->Add(*String::cast(*name_)->ToCString()); 4313 if (!name_.is_null()) stream->Add(*String::cast(*name_)->ToCString());
3868 stream->Add("[backing-store]"); 4314 stream->Add("[backing-store]");
3869 break; 4315 break;
3870 } 4316 }
3871 4317
3872 stream->Add("@%d", offset()); 4318 stream->Add("@%d", offset());
3873 } 4319 }
3874 4320
3875 } } // namespace v8::internal 4321 } } // namespace v8::internal
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698