OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "components/data_reduction_proxy/core/browser/data_reduction_proxy_usag
e_stats.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/callback.h" | |
9 #include "base/metrics/histogram.h" | |
10 #include "base/metrics/sparse_histogram.h" | |
11 #include "base/prefs/pref_member.h" | |
12 #include "base/single_thread_task_runner.h" | |
13 #include "components/data_reduction_proxy/core/browser/data_reduction_proxy_conf
ig.h" | |
14 #include "components/data_reduction_proxy/core/browser/data_reduction_proxy_tamp
er_detection.h" | |
15 #include "components/data_reduction_proxy/core/common/data_reduction_proxy_heade
rs.h" | |
16 #include "components/data_reduction_proxy/core/common/data_reduction_proxy_param
s.h" | |
17 #include "net/base/load_flags.h" | |
18 #include "net/base/net_errors.h" | |
19 #include "net/http/http_response_headers.h" | |
20 #include "net/http/http_status_code.h" | |
21 #include "net/proxy/proxy_server.h" | |
22 #include "net/proxy/proxy_service.h" | |
23 #include "net/url_request/url_request.h" | |
24 #include "net/url_request/url_request_context.h" | |
25 | |
26 using base::MessageLoopProxy; | |
27 using net::HostPortPair; | |
28 using net::ProxyServer; | |
29 using net::ProxyService; | |
30 using net::NetworkChangeNotifier; | |
31 using net::URLRequest; | |
32 | |
33 namespace data_reduction_proxy { | |
34 | |
35 namespace { | |
36 | |
37 const int kMinFailedRequestsWhenUnavailable = 1; | |
38 const int kMaxSuccessfulRequestsWhenUnavailable = 0; | |
39 const int kMaxFailedRequestsBeforeReset = 3; | |
40 | |
41 // Records a net error code that resulted in bypassing the data reduction | |
42 // proxy (|is_primary| is true) or the data reduction proxy fallback. | |
43 void RecordDataReductionProxyBypassOnNetworkError( | |
44 bool is_primary, | |
45 const ProxyServer& proxy_server, | |
46 int net_error) { | |
47 if (is_primary) { | |
48 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
49 "DataReductionProxy.BypassOnNetworkErrorPrimary", | |
50 std::abs(net_error)); | |
51 return; | |
52 } | |
53 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
54 "DataReductionProxy.BypassOnNetworkErrorFallback", | |
55 std::abs(net_error)); | |
56 } | |
57 | |
58 } // namespace | |
59 | |
60 // static | |
61 void DataReductionProxyUsageStats::RecordDataReductionProxyBypassInfo( | |
62 bool is_primary, | |
63 bool bypass_all, | |
64 const net::ProxyServer& proxy_server, | |
65 DataReductionProxyBypassType bypass_type) { | |
66 if (bypass_all) { | |
67 if (is_primary) { | |
68 UMA_HISTOGRAM_ENUMERATION("DataReductionProxy.BlockTypePrimary", | |
69 bypass_type, BYPASS_EVENT_TYPE_MAX); | |
70 } else { | |
71 UMA_HISTOGRAM_ENUMERATION("DataReductionProxy.BlockTypeFallback", | |
72 bypass_type, BYPASS_EVENT_TYPE_MAX); | |
73 } | |
74 } else { | |
75 if (is_primary) { | |
76 UMA_HISTOGRAM_ENUMERATION("DataReductionProxy.BypassTypePrimary", | |
77 bypass_type, BYPASS_EVENT_TYPE_MAX); | |
78 } else { | |
79 UMA_HISTOGRAM_ENUMERATION("DataReductionProxy.BypassTypeFallback", | |
80 bypass_type, BYPASS_EVENT_TYPE_MAX); | |
81 } | |
82 } | |
83 } | |
84 | |
85 // static | |
86 void DataReductionProxyUsageStats::DetectAndRecordMissingViaHeaderResponseCode( | |
87 bool is_primary, | |
88 const net::HttpResponseHeaders* headers) { | |
89 if (HasDataReductionProxyViaHeader(headers, NULL)) { | |
90 // The data reduction proxy via header is present, so don't record anything. | |
91 return; | |
92 } | |
93 | |
94 if (is_primary) { | |
95 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
96 "DataReductionProxy.MissingViaHeader.ResponseCode.Primary", | |
97 headers->response_code()); | |
98 } else { | |
99 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
100 "DataReductionProxy.MissingViaHeader.ResponseCode.Fallback", | |
101 headers->response_code()); | |
102 } | |
103 } | |
104 | |
105 DataReductionProxyUsageStats::DataReductionProxyUsageStats( | |
106 DataReductionProxyConfig* config, | |
107 UnreachableCallback unreachable_callback, | |
108 const scoped_refptr<base::SingleThreadTaskRunner>& ui_task_runner) | |
109 : data_reduction_proxy_config_(config), | |
110 unreachable_callback_(unreachable_callback), | |
111 last_bypass_type_(BYPASS_EVENT_TYPE_MAX), | |
112 triggering_request_(true), | |
113 ui_task_runner_(ui_task_runner), | |
114 successful_requests_through_proxy_count_(0), | |
115 proxy_net_errors_count_(0), | |
116 unavailable_(false) { | |
117 DCHECK(config); | |
118 NetworkChangeNotifier::AddNetworkChangeObserver(this); | |
119 }; | |
120 | |
121 DataReductionProxyUsageStats::~DataReductionProxyUsageStats() { | |
122 NetworkChangeNotifier::RemoveNetworkChangeObserver(this); | |
123 }; | |
124 | |
125 void DataReductionProxyUsageStats::OnUrlRequestCompleted( | |
126 const net::URLRequest* request, bool started) { | |
127 DCHECK(thread_checker_.CalledOnValidThread()); | |
128 | |
129 DataReductionProxyTypeInfo proxy_info; | |
130 // Ignore requests that did not use the data reduction proxy. The check for | |
131 // LOAD_BYPASS_PROXY is necessary because the proxy_server() in the |request| | |
132 // might still be set to the data reduction proxy if |request| was retried | |
133 // over direct and a network error occurred while retrying it. | |
134 if (data_reduction_proxy_config_->WasDataReductionProxyUsed(request, | |
135 &proxy_info) && | |
136 (request->load_flags() & net::LOAD_BYPASS_PROXY) == 0) { | |
137 if (request->status().status() == net::URLRequestStatus::SUCCESS) { | |
138 successful_requests_through_proxy_count_++; | |
139 NotifyUnavailabilityIfChanged(); | |
140 } | |
141 | |
142 // Report any network errors that occurred for this request, including OK | |
143 // and ABORTED. | |
144 if (!proxy_info.is_fallback) { | |
145 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
146 "DataReductionProxy.RequestCompletionErrorCodes.Primary", | |
147 std::abs(request->status().error())); | |
148 if (request->load_flags() & net::LOAD_MAIN_FRAME) { | |
149 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
150 "DataReductionProxy.RequestCompletionErrorCodes.MainFrame.Primary", | |
151 std::abs(request->status().error())); | |
152 } | |
153 } else { | |
154 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
155 "DataReductionProxy.RequestCompletionErrorCodes.Fallback", | |
156 std::abs(request->status().error())); | |
157 if (request->load_flags() & net::LOAD_MAIN_FRAME) { | |
158 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
159 "DataReductionProxy.RequestCompletionErrorCodes.MainFrame.Fallback", | |
160 std::abs(request->status().error())); | |
161 } | |
162 } | |
163 } | |
164 } | |
165 | |
166 void DataReductionProxyUsageStats::SetBypassType( | |
167 DataReductionProxyBypassType type) { | |
168 last_bypass_type_ = type; | |
169 triggering_request_ = true; | |
170 } | |
171 | |
172 DataReductionProxyBypassType | |
173 DataReductionProxyUsageStats::GetBypassType() const { | |
174 return last_bypass_type_; | |
175 } | |
176 | |
177 void DataReductionProxyUsageStats::RecordBytesHistograms( | |
178 const net::URLRequest& request, | |
179 const BooleanPrefMember& data_reduction_proxy_enabled, | |
180 const net::ProxyConfig& data_reduction_proxy_config) { | |
181 RecordBypassedBytesHistograms(request, data_reduction_proxy_enabled, | |
182 data_reduction_proxy_config); | |
183 RecordMissingViaHeaderBytes(request); | |
184 } | |
185 | |
186 void DataReductionProxyUsageStats::OnProxyFallback( | |
187 const net::ProxyServer& bypassed_proxy, | |
188 int net_error) { | |
189 DataReductionProxyTypeInfo data_reduction_proxy_info; | |
190 if (bypassed_proxy.is_valid() && !bypassed_proxy.is_direct() && | |
191 data_reduction_proxy_config_->IsDataReductionProxy( | |
192 bypassed_proxy.host_port_pair(), &data_reduction_proxy_info)) { | |
193 if (data_reduction_proxy_info.is_ssl) | |
194 return; | |
195 | |
196 proxy_net_errors_count_++; | |
197 | |
198 // To account for the case when the proxy is reachable for sometime, and | |
199 // then gets blocked, we reset counts when number of errors exceed | |
200 // the threshold. | |
201 if (proxy_net_errors_count_ >= kMaxFailedRequestsBeforeReset && | |
202 successful_requests_through_proxy_count_ > | |
203 kMaxSuccessfulRequestsWhenUnavailable) { | |
204 ClearRequestCounts(); | |
205 } else { | |
206 NotifyUnavailabilityIfChanged(); | |
207 } | |
208 | |
209 if (!data_reduction_proxy_info.is_fallback) { | |
210 RecordDataReductionProxyBypassInfo( | |
211 true, false, bypassed_proxy, BYPASS_EVENT_TYPE_NETWORK_ERROR); | |
212 RecordDataReductionProxyBypassOnNetworkError( | |
213 true, bypassed_proxy, net_error); | |
214 } else { | |
215 RecordDataReductionProxyBypassInfo( | |
216 false, false, bypassed_proxy, BYPASS_EVENT_TYPE_NETWORK_ERROR); | |
217 RecordDataReductionProxyBypassOnNetworkError( | |
218 false, bypassed_proxy, net_error); | |
219 } | |
220 } | |
221 } | |
222 | |
223 void DataReductionProxyUsageStats::OnConnectComplete( | |
224 const net::HostPortPair& proxy_server, | |
225 int net_error) { | |
226 if (data_reduction_proxy_config_->IsDataReductionProxy(proxy_server, NULL)) { | |
227 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
228 "DataReductionProxy.HTTPConnectCompleted", | |
229 std::abs(net_error)); | |
230 } | |
231 } | |
232 | |
233 void DataReductionProxyUsageStats::RecordBypassedBytesHistograms( | |
234 const net::URLRequest& request, | |
235 const BooleanPrefMember& data_reduction_proxy_enabled, | |
236 const net::ProxyConfig& data_reduction_proxy_config) { | |
237 int64 content_length = request.received_response_content_length(); | |
238 | |
239 // Only record histograms when the data reduction proxy is enabled. | |
240 if (!data_reduction_proxy_enabled.GetValue()) | |
241 return; | |
242 | |
243 // TODO(bengr): Add histogram(s) for byte counts of unsupported schemes, e.g., | |
244 // ws and wss. | |
245 if (!request.url().SchemeIsHTTPOrHTTPS()) | |
246 return; | |
247 | |
248 DataReductionProxyTypeInfo data_reduction_proxy_type_info; | |
249 if (data_reduction_proxy_config_->WasDataReductionProxyUsed( | |
250 &request, &data_reduction_proxy_type_info)) { | |
251 RecordBypassedBytes(last_bypass_type_, | |
252 DataReductionProxyUsageStats::NOT_BYPASSED, | |
253 content_length); | |
254 | |
255 // If non-empty, |proxy_server.first| is the proxy that this request used. | |
256 const net::ProxyServer& first = | |
257 data_reduction_proxy_type_info.proxy_servers.first; | |
258 if (first.is_valid() && !first.host_port_pair().IsEmpty()) { | |
259 DataReductionProxyTamperDetection::DetectAndReport( | |
260 request.response_info().headers.get(), | |
261 first.is_https() || first.is_quic(), content_length); | |
262 } | |
263 return; | |
264 } | |
265 | |
266 if (request.url().SchemeIs(url::kHttpsScheme)) { | |
267 RecordBypassedBytes(last_bypass_type_, | |
268 DataReductionProxyUsageStats::SSL, | |
269 content_length); | |
270 return; | |
271 } | |
272 | |
273 // Now that the data reduction proxy is a best effort proxy, if the effective | |
274 // proxy configuration resolves to anything other than direct:// for a URL, | |
275 // the data reduction proxy will not be used. | |
276 DCHECK(!data_reduction_proxy_type_info.proxy_servers.first.is_valid()); | |
277 if (!request.proxy_server().IsEmpty()) { | |
278 RecordBypassedBytes(last_bypass_type_, | |
279 DataReductionProxyUsageStats::PROXY_OVERRIDDEN, | |
280 content_length); | |
281 return; | |
282 } | |
283 | |
284 if (data_reduction_proxy_config_->IsBypassedByDataReductionProxyLocalRules( | |
285 request, data_reduction_proxy_config)) { | |
286 RecordBypassedBytes(last_bypass_type_, | |
287 DataReductionProxyUsageStats::LOCAL_BYPASS_RULES, | |
288 content_length); | |
289 return; | |
290 } | |
291 | |
292 // Only record separate triggering request UMA for short, medium, and long | |
293 // bypass events. | |
294 if (triggering_request_ && | |
295 (last_bypass_type_ == BYPASS_EVENT_TYPE_SHORT || | |
296 last_bypass_type_ == BYPASS_EVENT_TYPE_MEDIUM || | |
297 last_bypass_type_ == BYPASS_EVENT_TYPE_LONG)) { | |
298 std::string mime_type; | |
299 request.GetMimeType(&mime_type); | |
300 // MIME types are named by <media-type>/<subtype>. Check to see if the | |
301 // media type is audio or video. Only record when triggered by short bypass, | |
302 // there isn't an audio or video bucket for medium or long bypasses. | |
303 if (last_bypass_type_ == BYPASS_EVENT_TYPE_SHORT && | |
304 (mime_type.compare(0, 6, "audio/") == 0 || | |
305 mime_type.compare(0, 6, "video/") == 0)) { | |
306 RecordBypassedBytes(last_bypass_type_, | |
307 DataReductionProxyUsageStats::AUDIO_VIDEO, | |
308 content_length); | |
309 return; | |
310 } | |
311 | |
312 RecordBypassedBytes(last_bypass_type_, | |
313 DataReductionProxyUsageStats::TRIGGERING_REQUEST, | |
314 content_length); | |
315 triggering_request_ = false; | |
316 return; | |
317 } | |
318 | |
319 if (last_bypass_type_ != BYPASS_EVENT_TYPE_MAX) { | |
320 RecordBypassedBytes(last_bypass_type_, | |
321 DataReductionProxyUsageStats::BYPASSED_BYTES_TYPE_MAX, | |
322 content_length); | |
323 return; | |
324 } | |
325 | |
326 if (data_reduction_proxy_config_->AreDataReductionProxiesBypassed( | |
327 request, data_reduction_proxy_config, NULL)) { | |
328 RecordBypassedBytes(last_bypass_type_, | |
329 DataReductionProxyUsageStats::NETWORK_ERROR, | |
330 content_length); | |
331 } | |
332 } | |
333 | |
334 void DataReductionProxyUsageStats::RecordMissingViaHeaderBytes( | |
335 const URLRequest& request) { | |
336 // Responses that were served from cache should have been filtered out | |
337 // already. | |
338 DCHECK(!request.was_cached()); | |
339 | |
340 if (!data_reduction_proxy_config_->WasDataReductionProxyUsed(&request, | |
341 NULL) || | |
342 HasDataReductionProxyViaHeader(request.response_headers(), NULL)) { | |
343 // Only track requests that used the data reduction proxy and had responses | |
344 // that were missing the data reduction proxy via header. | |
345 return; | |
346 } | |
347 | |
348 if (request.GetResponseCode() >= net::HTTP_BAD_REQUEST && | |
349 request.GetResponseCode() < net::HTTP_INTERNAL_SERVER_ERROR) { | |
350 // Track 4xx responses that are missing via headers separately. | |
351 UMA_HISTOGRAM_COUNTS("DataReductionProxy.MissingViaHeader.Bytes.4xx", | |
352 request.received_response_content_length()); | |
353 } else { | |
354 UMA_HISTOGRAM_COUNTS("DataReductionProxy.MissingViaHeader.Bytes.Other", | |
355 request.received_response_content_length()); | |
356 } | |
357 } | |
358 | |
359 void DataReductionProxyUsageStats::OnNetworkChanged( | |
360 NetworkChangeNotifier::ConnectionType type) { | |
361 DCHECK(thread_checker_.CalledOnValidThread()); | |
362 ClearRequestCounts(); | |
363 } | |
364 | |
365 void DataReductionProxyUsageStats::ClearRequestCounts() { | |
366 DCHECK(thread_checker_.CalledOnValidThread()); | |
367 successful_requests_through_proxy_count_ = 0; | |
368 proxy_net_errors_count_ = 0; | |
369 } | |
370 | |
371 void DataReductionProxyUsageStats::NotifyUnavailabilityIfChanged() { | |
372 bool prev_unavailable = unavailable_; | |
373 unavailable_ = | |
374 (proxy_net_errors_count_ >= kMinFailedRequestsWhenUnavailable && | |
375 successful_requests_through_proxy_count_ <= | |
376 kMaxSuccessfulRequestsWhenUnavailable); | |
377 if (prev_unavailable != unavailable_) { | |
378 ui_task_runner_->PostTask(FROM_HERE, base::Bind( | |
379 &DataReductionProxyUsageStats::NotifyUnavailabilityOnUIThread, | |
380 base::Unretained(this), | |
381 unavailable_)); | |
382 } | |
383 } | |
384 | |
385 void DataReductionProxyUsageStats::NotifyUnavailabilityOnUIThread( | |
386 bool unavailable) { | |
387 DCHECK(ui_task_runner_->BelongsToCurrentThread()); | |
388 unreachable_callback_.Run(unavailable); | |
389 } | |
390 | |
391 void DataReductionProxyUsageStats::RecordBypassedBytes( | |
392 DataReductionProxyBypassType bypass_type, | |
393 DataReductionProxyUsageStats::BypassedBytesType bypassed_bytes_type, | |
394 int64 content_length) { | |
395 // Individual histograms are needed to count the bypassed bytes for each | |
396 // bypass type so that we can see the size of requests. This helps us | |
397 // remove outliers that would skew the sum of bypassed bytes for each type. | |
398 switch (bypassed_bytes_type) { | |
399 case DataReductionProxyUsageStats::NOT_BYPASSED: | |
400 UMA_HISTOGRAM_COUNTS( | |
401 "DataReductionProxy.BypassedBytes.NotBypassed", content_length); | |
402 break; | |
403 case DataReductionProxyUsageStats::SSL: | |
404 UMA_HISTOGRAM_COUNTS( | |
405 "DataReductionProxy.BypassedBytes.SSL", content_length); | |
406 break; | |
407 case DataReductionProxyUsageStats::LOCAL_BYPASS_RULES: | |
408 UMA_HISTOGRAM_COUNTS( | |
409 "DataReductionProxy.BypassedBytes.LocalBypassRules", | |
410 content_length); | |
411 break; | |
412 case DataReductionProxyUsageStats::PROXY_OVERRIDDEN: | |
413 UMA_HISTOGRAM_COUNTS( | |
414 "DataReductionProxy.BypassedBytes.ProxyOverridden", | |
415 content_length); | |
416 break; | |
417 case DataReductionProxyUsageStats::AUDIO_VIDEO: | |
418 if (last_bypass_type_ == BYPASS_EVENT_TYPE_SHORT) { | |
419 UMA_HISTOGRAM_COUNTS( | |
420 "DataReductionProxy.BypassedBytes.ShortAudioVideo", | |
421 content_length); | |
422 } | |
423 break; | |
424 case DataReductionProxyUsageStats::TRIGGERING_REQUEST: | |
425 switch (bypass_type) { | |
426 case BYPASS_EVENT_TYPE_SHORT: | |
427 UMA_HISTOGRAM_COUNTS( | |
428 "DataReductionProxy.BypassedBytes.ShortTriggeringRequest", | |
429 content_length); | |
430 break; | |
431 case BYPASS_EVENT_TYPE_MEDIUM: | |
432 UMA_HISTOGRAM_COUNTS( | |
433 "DataReductionProxy.BypassedBytes.MediumTriggeringRequest", | |
434 content_length); | |
435 break; | |
436 case BYPASS_EVENT_TYPE_LONG: | |
437 UMA_HISTOGRAM_COUNTS( | |
438 "DataReductionProxy.BypassedBytes.LongTriggeringRequest", | |
439 content_length); | |
440 break; | |
441 default: | |
442 break; | |
443 } | |
444 break; | |
445 case DataReductionProxyUsageStats::NETWORK_ERROR: | |
446 UMA_HISTOGRAM_COUNTS( | |
447 "DataReductionProxy.BypassedBytes.NetworkErrorOther", | |
448 content_length); | |
449 break; | |
450 case DataReductionProxyUsageStats::BYPASSED_BYTES_TYPE_MAX: | |
451 switch (bypass_type) { | |
452 case BYPASS_EVENT_TYPE_CURRENT: | |
453 UMA_HISTOGRAM_COUNTS("DataReductionProxy.BypassedBytes.Current", | |
454 content_length); | |
455 break; | |
456 case BYPASS_EVENT_TYPE_SHORT: | |
457 UMA_HISTOGRAM_COUNTS("DataReductionProxy.BypassedBytes.ShortAll", | |
458 content_length); | |
459 break; | |
460 case BYPASS_EVENT_TYPE_MEDIUM: | |
461 UMA_HISTOGRAM_COUNTS("DataReductionProxy.BypassedBytes.MediumAll", | |
462 content_length); | |
463 break; | |
464 case BYPASS_EVENT_TYPE_LONG: | |
465 UMA_HISTOGRAM_COUNTS("DataReductionProxy.BypassedBytes.LongAll", | |
466 content_length); | |
467 break; | |
468 case BYPASS_EVENT_TYPE_MISSING_VIA_HEADER_4XX: | |
469 UMA_HISTOGRAM_COUNTS( | |
470 "DataReductionProxy.BypassedBytes.MissingViaHeader4xx", | |
471 content_length); | |
472 break; | |
473 case BYPASS_EVENT_TYPE_MISSING_VIA_HEADER_OTHER: | |
474 UMA_HISTOGRAM_COUNTS( | |
475 "DataReductionProxy.BypassedBytes.MissingViaHeaderOther", | |
476 content_length); | |
477 break; | |
478 case BYPASS_EVENT_TYPE_MALFORMED_407: | |
479 UMA_HISTOGRAM_COUNTS("DataReductionProxy.BypassedBytes.Malformed407", | |
480 content_length); | |
481 break; | |
482 case BYPASS_EVENT_TYPE_STATUS_500_HTTP_INTERNAL_SERVER_ERROR: | |
483 UMA_HISTOGRAM_COUNTS( | |
484 "DataReductionProxy.BypassedBytes." | |
485 "Status500HttpInternalServerError", | |
486 content_length); | |
487 break; | |
488 case BYPASS_EVENT_TYPE_STATUS_502_HTTP_BAD_GATEWAY: | |
489 UMA_HISTOGRAM_COUNTS( | |
490 "DataReductionProxy.BypassedBytes.Status502HttpBadGateway", | |
491 content_length); | |
492 break; | |
493 case BYPASS_EVENT_TYPE_STATUS_503_HTTP_SERVICE_UNAVAILABLE: | |
494 UMA_HISTOGRAM_COUNTS( | |
495 "DataReductionProxy.BypassedBytes." | |
496 "Status503HttpServiceUnavailable", | |
497 content_length); | |
498 break; | |
499 default: | |
500 break; | |
501 } | |
502 break; | |
503 } | |
504 } | |
505 | |
506 } // namespace data_reduction_proxy | |
507 | |
508 | |
OLD | NEW |