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.
5 #import "ios/chrome/browser/memory/memory_debugger.h"
7 #include "base/ios/ios_util.h"
8 #import "base/mac/scoped_nsobject.h"
9 #import "base/memory/scoped_ptr.h"
10 #import "ios/chrome/browser/memory/memory_metrics.h"
11 #include "ios/chrome/browser/ui/ui_util.h"
12 #import "ios/chrome/browser/ui/uikit_ui_util.h"
15 // The number of bytes in a megabyte.
16 const CGFloat kNumBytesInMB = 1024 * 1024;
17 // The horizontal and vertical padding between subviews.
18 const CGFloat kPadding = 10;
21 @implementation MemoryDebugger {
22 // A timer to trigger refreshes.
23 base::scoped_nsobject<NSTimer> _refreshTimer;
25 // A timer to trigger continuous memory warnings.
26 base::scoped_nsobject<NSTimer> _memoryWarningTimer;
29 base::scoped_nsobject<UIFont> _font;
31 // Labels for memory metrics.
32 base::scoped_nsobject<UILabel> _physicalFreeMemoryLabel;
33 base::scoped_nsobject<UILabel> _realMemoryUsedLabel;
34 base::scoped_nsobject<UILabel> _xcodeGaugeLabel;
35 base::scoped_nsobject<UILabel> _dirtyVirtualMemoryLabel;
37 // Inputs for memory commands.
38 base::scoped_nsobject<UITextField> _bloatField;
39 base::scoped_nsobject<UITextField> _refreshField;
40 base::scoped_nsobject<UITextField> _continuousMemoryWarningField;
42 // A place to store the artifical memory bloat.
43 scoped_ptr<uint8> _bloat;
45 // Distance the view was pushed up to accomodate the keyboard.
46 CGFloat _keyboardOffset;
48 // The current orientation of the device.
49 BOOL _currentOrientation;
52 - (instancetype)init {
53 self = [super initWithFrame:CGRectZero];
55 _font.reset([[UIFont systemFontOfSize:14] retain]);
56 self.backgroundColor = [UIColor colorWithWhite:0.8f alpha:0.9f];
60 [self adjustForOrientation:nil];
62 [self registerForNotifications];
67 // NSTimers create a retain cycle so they must be invalidated before this
68 // instance can be deallocated.
69 - (void)invalidateTimers {
70 [_refreshTimer invalidate];
71 [_memoryWarningTimer invalidate];
75 [[NSNotificationCenter defaultCenter] removeObserver:self];
79 #pragma mark UIView methods
81 - (CGSize)sizeThatFits:(CGSize)size {
84 for (UIView* subview in self.subviews) {
85 width = MAX(width, CGRectGetMaxX(subview.frame));
86 height = MAX(height, CGRectGetMaxY(subview.frame));
88 return CGSizeMake(width + kPadding, height + kPadding);
91 #pragma mark initialization helpers
94 // |index| is used to calculate the vertical position of each element in
98 // Display some metrics.
99 _physicalFreeMemoryLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
100 [self addMetricWithName:@"Physical Free"
102 usingLabel:_physicalFreeMemoryLabel];
103 _realMemoryUsedLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
104 [self addMetricWithName:@"Real Memory Used"
106 usingLabel:_realMemoryUsedLabel];
107 _xcodeGaugeLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
108 [self addMetricWithName:@"Xcode Gauge"
110 usingLabel:_xcodeGaugeLabel];
111 _dirtyVirtualMemoryLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
112 [self addMetricWithName:@"Dirty VM"
114 usingLabel:_dirtyVirtualMemoryLabel];
116 // Since _performMemoryWarning is a private API it can't be compiled into
118 // TODO(lliabraa): Figure out how to support memory warnings (or something
119 // like them) in official builds.
121 [self addButtonWithTitle:@"Trigger Memory Warning"
122 target:[UIApplication sharedApplication]
123 action:@selector(_performMemoryWarning)
124 withOrigin:[self originForSubviewAtIndex:index++]];
125 #endif // CHROMIUM_BUILD
127 // Display a text input to set the amount of artificial memory bloat and a
128 // button to reset the bloat to zero.
129 _bloatField.reset([[UITextField alloc] initWithFrame:CGRectZero]);
130 [self addLabelWithText:@"Set bloat (MB)"
133 inputAction:@selector(updateBloat)
134 buttonWithTitle:@"Clear"
136 buttonAction:@selector(clearBloat)
138 [_bloatField setText:@"0"];
141 // Since _performMemoryWarning is a private API it can't be compiled into
143 // TODO(lliabraa): Figure out how to support memory warnings (or something
144 // like them) in official builds.
146 // Display a text input to control the rate of continuous memory warnings.
147 _continuousMemoryWarningField.reset(
148 [[UITextField alloc] initWithFrame:CGRectZero]);
149 [self addLabelWithText:@"Set memory warning interval (secs)"
150 input:_continuousMemoryWarningField
152 inputAction:@selector(updateMemoryWarningInterval)
154 [_continuousMemoryWarningField setText:@"0.0"];
155 #endif // CHROMIUM_BUILD
157 // Display a text input to control the refresh rate of the memory debugger.
158 _refreshField.reset([[UITextField alloc] initWithFrame:CGRectZero]);
159 [self addLabelWithText:@"Set refresh interval (secs)"
162 inputAction:@selector(updateRefreshInterval)
164 [_refreshField setText:@"0.5"];
165 [self updateRefreshInterval];
168 - (void)registerForNotifications {
169 // On iOS 7, the screen coordinate system is not dependent on orientation so
170 // the debugger has to handle its own rotation.
171 if (!base::ios::IsRunningOnIOS8OrLater()) {
172 // Register to receive orientation notifications.
173 [[NSNotificationCenter defaultCenter]
175 selector:@selector(adjustForOrientation:)
176 name:UIDeviceOrientationDidChangeNotification
180 // Register to receive memory warning.
181 [[NSNotificationCenter defaultCenter]
183 selector:@selector(lowMemoryWarningReceived:)
184 name:UIApplicationDidReceiveMemoryWarningNotification
187 // Register to receive keyboard will show notification.
188 [[NSNotificationCenter defaultCenter]
190 selector:@selector(keyboardWillShow:)
191 name:UIKeyboardWillShowNotification
194 // Register to receive keyboard will hide notification.
195 [[NSNotificationCenter defaultCenter]
197 selector:@selector(keyboardWillHide:)
198 name:UIKeyboardWillHideNotification
202 // Adds subviews for the specified metric, the value of which will be displayed
204 - (void)addMetricWithName:(NSString*)name
205 atIndex:(NSUInteger)index
206 usingLabel:(UILabel*)label {
207 // The width of the view for the metric's name.
208 const CGFloat kNameWidth = 150;
209 // The width of the view for each metric.
210 const CGFloat kMetricWidth = 100;
211 CGPoint nameOrigin = [self originForSubviewAtIndex:index];
213 CGRectMake(nameOrigin.x, nameOrigin.y, kNameWidth, [_font lineHeight]);
214 base::scoped_nsobject<UILabel> nameLabel(
215 [[UILabel alloc] initWithFrame:nameFrame]);
216 [nameLabel setText:[NSString stringWithFormat:@"%@: ", name]];
217 [nameLabel setFont:_font];
218 [self addSubview:nameLabel];
219 label.frame = CGRectMake(CGRectGetMaxX(nameFrame), nameFrame.origin.y,
220 kMetricWidth, [_font lineHeight]);
221 [label setFont:_font];
222 [label setTextAlignment:NSTextAlignmentRight];
223 [self addSubview:label];
226 // Adds a subview for a button with the given title and target/action.
227 - (void)addButtonWithTitle:(NSString*)title
230 withOrigin:(CGPoint)origin {
231 base::scoped_nsobject<UIButton> button(
232 [[UIButton buttonWithType:UIButtonTypeSystem] retain]);
233 [button setTitle:title forState:UIControlStateNormal];
234 [button titleLabel].font = _font;
235 [[button titleLabel] setTextAlignment:NSTextAlignmentCenter];
237 [button setFrame:CGRectMake(origin.x, origin.y, [button frame].size.width,
238 [_font lineHeight])];
239 [button addTarget:target
241 forControlEvents:UIControlEventTouchUpInside];
242 [self addSubview:button];
245 // Adds subviews for a UI component with label and input text field.
247 // -------------------------
248 // | labelText | <input> |
249 // -------------------------
251 // The inputTarget/inputAction will be invoked when the user finishes editing
253 - (void)addLabelWithText:(NSString*)labelText
254 input:(UITextField*)input
255 inputTarget:(id)inputTarget
256 inputAction:(SEL)inputAction
257 atIndex:(NSUInteger)index {
258 [self addLabelWithText:labelText
260 inputTarget:inputTarget
261 inputAction:inputAction
268 // Adds subviews for a UI component with label, input text field and button.
270 // -------------------------------------
271 // | labelText | <input> | <button> |
272 // -------------------------------------
274 // The inputTarget/inputAction will be invoked when the user finishes editing
276 - (void)addLabelWithText:(NSString*)labelText
277 input:(UITextField*)input
278 inputTarget:(id)inputTarget
279 inputAction:(SEL)inputAction
280 buttonWithTitle:(NSString*)buttonTitle
281 buttonTarget:(id)buttonTarget
282 buttonAction:(SEL)buttonAction
283 atIndex:(NSUInteger)index {
284 base::scoped_nsobject<UILabel> label(
285 [[UILabel alloc] initWithFrame:CGRectZero]);
287 [label setText:[NSString stringWithFormat:@"%@: ", labelText]];
289 [label setFont:_font];
291 CGPoint labelOrigin = [self originForSubviewAtIndex:index];
292 [label setFrame:CGRectOffset([label frame], labelOrigin.x, labelOrigin.y)];
293 [self addSubview:label];
295 // The width of the views for each input text field.
296 const CGFloat kInputWidth = 50;
298 CGRectMake(CGRectGetMaxX([label frame]) + kPadding,
299 [label frame].origin.y, kInputWidth, [_font lineHeight]);
301 input.backgroundColor = [UIColor whiteColor];
302 input.delegate = self;
303 input.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
304 input.adjustsFontSizeToFitWidth = YES;
305 input.textAlignment = NSTextAlignmentRight;
306 [input addTarget:inputTarget
308 forControlEvents:UIControlEventEditingDidEnd];
310 [self addSubview:input];
314 const CGFloat kButtonXOffset =
315 input ? CGRectGetMaxX(input.frame) : CGRectGetMaxX([label frame]);
317 CGPointMake(kButtonXOffset + kPadding, [label frame].origin.y);
318 [self addButtonWithTitle:buttonTitle
325 // Returns the CGPoint of the origin of the subview at |index|.
326 - (CGPoint)originForSubviewAtIndex:(NSUInteger)index {
327 return CGPointMake(kPadding,
328 (index + 1) * kPadding + index * [_font lineHeight]);
331 #pragma mark Refresh callback
333 // Updates content and ensures the view is visible.
334 - (void)refresh:(NSTimer*)timer {
335 [self.superview bringSubviewToFront:self];
336 [self updateMemoryInfo];
339 #pragma mark Memory inspection
341 // Updates the memory metrics shown.
342 - (void)updateMemoryInfo {
343 CGFloat value = memory_util::GetFreePhysicalBytes() / kNumBytesInMB;
344 [_physicalFreeMemoryLabel
345 setText:[NSString stringWithFormat:@"%.2f MB", value]];
346 value = memory_util::GetRealMemoryUsedInBytes() / kNumBytesInMB;
347 [_realMemoryUsedLabel setText:[NSString stringWithFormat:@"%.2f MB", value]];
348 value = memory_util::GetInternalVMBytes() / kNumBytesInMB;
349 [_xcodeGaugeLabel setText:[NSString stringWithFormat:@"%.2f MB", value]];
350 value = memory_util::GetDirtyVMBytes() / kNumBytesInMB;
351 [_dirtyVirtualMemoryLabel
352 setText:[NSString stringWithFormat:@"%.2f MB", value]];
355 #pragma mark Memory Warning notification callback
357 // Flashes the debugger to indicate memory warning.
358 - (void)lowMemoryWarningReceived:(NSNotification*)notification {
359 UIColor* originalColor = self.backgroundColor;
360 self.backgroundColor =
361 [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.9];
362 [UIView animateWithDuration:1.0
364 options:UIViewAnimationOptionAllowUserInteraction
366 self.backgroundColor = originalColor;
371 #pragma mark Rotation notification callback
373 - (void)didMoveToSuperview {
374 UIView* superview = [self superview];
376 [self setCenter:[superview center]];
379 - (void)adjustForOrientation:(NSNotification*)notification {
380 if (base::ios::IsRunningOnIOS8OrLater()) {
383 UIInterfaceOrientation orientation =
384 [[UIApplication sharedApplication] statusBarOrientation];
385 if (orientation == _currentOrientation) {
388 _currentOrientation = orientation;
390 switch (orientation) {
391 case UIInterfaceOrientationPortrait:
394 case UIInterfaceOrientationPortraitUpsideDown:
397 case UIInterfaceOrientationLandscapeLeft:
400 case UIInterfaceOrientationLandscapeRight:
403 case UIInterfaceOrientationUnknown:
408 // Since the debugger view is in screen coordinates and handles its own
409 // rotation via the |transform| property, the view's position after rotation
410 // can be unexpected and partially off-screen. Centering the view before
411 // rotating it ensures that the view remains within the bounds of the screen.
412 if (self.superview) {
413 self.center = self.superview.center;
415 self.transform = CGAffineTransformMakeRotation(angle);
418 #pragma mark Keyboard notification callbacks
420 // Ensures the debugger is visible by shifting it up as the keyboard animates
422 - (void)keyboardWillShow:(NSNotification*)notification {
423 NSDictionary* userInfo = [notification userInfo];
424 NSValue* keyboardFrameValue =
425 [userInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
426 CGFloat keyboardHeight = CurrentKeyboardHeight(keyboardFrameValue);
428 // Get the coord of the bottom of the debugger's frame. This is orientation
429 // dependent on iOS 7 because the debugger is in screen coords.
430 CGFloat bottomOfFrame = CGRectGetMaxY(self.frame);
431 if (!base::ios::IsRunningOnIOS8OrLater() && IsLandscape())
432 bottomOfFrame = CGRectGetMaxX(self.frame);
434 // Shift the debugger up by the "height" of the keyboard, but since the
435 // keyboard rect is in screen coords, use the orientation to find the height.
436 CGFloat distanceFromBottom = CurrentScreenHeight() - bottomOfFrame;
437 _keyboardOffset = -1 * fmax(0.0f, keyboardHeight - distanceFromBottom);
438 [self animateForKeyboardNotification:notification
439 withOffset:CGPointMake(0, _keyboardOffset)];
442 // Shifts the debugger back down when the keyboard is hidden.
443 - (void)keyboardWillHide:(NSNotification*)notification {
444 [self animateForKeyboardNotification:notification
445 withOffset:CGPointMake(0, -_keyboardOffset)];
448 - (void)animateForKeyboardNotification:(NSNotification*)notification
449 withOffset:(CGPoint)offset {
450 // Account for orientation.
451 offset = CGPointApplyAffineTransform(offset, self.transform);
452 // Normally this would use an animation block, but there is no API to
453 // convert the UIKeyboardAnimationCurveUserInfoKey's value from a
454 // UIViewAnimationCurve to a UIViewAnimationOption. Awesome!
455 NSDictionary* userInfo = [notification userInfo];
456 [UIView beginAnimations:nil context:nullptr];
457 [UIView setAnimationDuration:
458 [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
459 NSInteger animationCurveKeyValue =
460 [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
461 UIViewAnimationCurve animationCurve =
462 (UIViewAnimationCurve)animationCurveKeyValue;
463 [UIView setAnimationCurve:animationCurve];
464 [UIView setAnimationBeginsFromCurrentState:YES];
465 self.frame = CGRectOffset(self.frame, offset.x, offset.y);
466 [UIView commitAnimations];
469 #pragma mark Artificial memory bloat methods
471 - (void)updateBloat {
473 NSScanner* scanner = [NSScanner scannerWithString:[_bloatField text]];
474 if (![scanner scanDouble:&bloatSizeMB] || bloatSizeMB < 0.0) {
476 NSString* errorMessage =
477 [NSString stringWithFormat:@"Invalid value \"%@\" for bloat size.\n"
478 @"Must be a positive number.\n"
479 @"Resetting to %.1f MB",
480 [_bloatField text], bloatSizeMB];
481 [self alert:errorMessage];
482 [_bloatField setText:[NSString stringWithFormat:@"%.1f", bloatSizeMB]];
484 const CGFloat kBloatSizeBytes = ceil(bloatSizeMB * kNumBytesInMB);
485 const uint64 kNumberOfBytes = static_cast<uint64>(kBloatSizeBytes);
486 _bloat.reset(kNumberOfBytes ? new uint8[kNumberOfBytes] : nullptr);
488 memset(_bloat.get(), -1, kNumberOfBytes); // Occupy memory.
490 if (kNumberOfBytes) {
491 [self alert:@"Could not allocate memory."];
497 [_bloatField setText:@"0"];
498 [_bloatField resignFirstResponder];
502 #pragma mark Refresh interval methods
504 - (void)updateRefreshInterval {
505 double refreshTimerValue;
506 NSScanner* scanner = [NSScanner scannerWithString:[_refreshField text]];
507 if (![scanner scanDouble:&refreshTimerValue] || refreshTimerValue < 0.0) {
508 refreshTimerValue = 0.5;
509 NSString* errorMessage = [NSString
510 stringWithFormat:@"Invalid value \"%@\" for refresh interval.\n"
511 @"Must be a positive number.\n" @"Resetting to %.1f",
512 [_refreshField text], refreshTimerValue];
513 [self alert:errorMessage];
515 setText:[NSString stringWithFormat:@"%.1f", refreshTimerValue]];
518 [_refreshTimer invalidate];
520 [[NSTimer scheduledTimerWithTimeInterval:refreshTimerValue
522 selector:@selector(refresh:)
524 repeats:YES] retain]);
527 #pragma mark Memory warning interval methods
529 // Since _performMemoryWarning is a private API it can't be compiled into
531 // TODO(lliabraa): Figure out how to support memory warnings (or something
532 // like them) in official builds.
534 - (void)updateMemoryWarningInterval {
535 [_memoryWarningTimer invalidate];
537 NSString* text = [_continuousMemoryWarningField text];
538 NSScanner* scanner = [NSScanner scannerWithString:text];
539 BOOL valueFound = [scanner scanDouble:&timerValue];
540 // If the text field is empty or contains 0, return early to turn off
541 // continuous memory warnings.
542 if (![text length] || timerValue == 0.0) {
545 // If no value could be parsed or a non-positive value was found, throw up an
546 // error message and return early to turn off continuous memory warnings.
547 if (!valueFound || timerValue <= 0.0) {
548 NSString* errorMessage = [NSString
549 stringWithFormat:@"Invalid value \"%@\" for memory warning interval.\n"
550 @"Must be a positive number.\n"
551 @"Turning off continuous memory warnings",
553 [self alert:errorMessage];
554 [_continuousMemoryWarningField setText:@""];
557 // If a valid value was found have the timer start triggering continuous
559 _memoryWarningTimer.reset(
560 [[NSTimer scheduledTimerWithTimeInterval:timerValue
561 target:[UIApplication sharedApplication]
562 selector:@selector(_performMemoryWarning)
564 repeats:YES] retain]);
566 #endif // CHROMIUM_BUILD
568 #pragma mark UITextViewDelegate methods
570 // Dismisses the keyboard if the user hits return.
571 - (BOOL)textFieldShouldReturn:(UITextField*)textField {
572 [textField resignFirstResponder];
576 #pragma mark UIResponder methods
578 // Allows the debugger to be dragged around the screen.
579 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
580 UITouch* touch = [touches anyObject];
581 CGPoint start = [touch previousLocationInView:self];
582 CGPoint end = [touch locationInView:self];
583 CGPoint offset = CGPointMake(end.x - start.x, end.y - start.y);
584 offset = CGPointApplyAffineTransform(offset, self.transform);
585 self.frame = CGRectOffset(self.frame, offset.x, offset.y);
588 #pragma mark Error handling
590 // Shows an alert with the given |errorMessage|.
591 - (void)alert:(NSString*)errorMessage {
592 UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Error"
595 cancelButtonTitle:@"OK"
596 otherButtonTitles:nil, nil];