animate - iOS/Core-Animation: Performance tuning




uiview animate layer properties (4)

I have my app running on my iPad. but it is performing very badly -- I am getting below 15fps. can anyone help me to optimise?

It is basically a wheel (derived from UIView) containing 12 buttons (derived from UIControl).

As the user spins it, the buttons dynamically expand and contract (e.g. the one at the 12 o'clock position should always be the biggest)

So my wheel contains a:

- (void) displayLinkIsCallingBack: (CADisplayLink *) dispLink 
{
    :
    // using CATransaction like this goes from 14fps to 19fps
    [CATransaction begin];
    [CATransaction setDisableActions: YES];

    // NEG, as coord system is flipped/fucked
    self.transform = CGAffineTransformMakeRotation(-thetaWheel);

    [CATransaction commit];

    if (BLA)
        [self rotateNotch: direction];  
}

… which calculates from recent touch input the new rotation for the wheel. There is already one performance issue here which I am pursuing on a separate thread: iOS Core-Animation: Performance issues with CATransaction / Interpolating transform matrices

This routine also checks whether the wheel has completed another 1/12 rotation, and if so instructs all 12 buttons to resize:

// Wheel.m
- (void) rotateNotch: (int) direction
{
    for (int i=0; i < [self buttonCount] ; i++)
    {
            CustomButton * b = (CustomButton *) [self.buttons objectAtIndex: i];

    // Note that b.btnSize is a dynamic property which will calculate
    // the desired button size based on the button index and the wheels rotation.
            [b resize: b.btnSize];
    }
}

Now for the actual resizing code, in button.m:

// Button.m

- (void) scaleFrom: (float) s_old
                to: (float) s_new
              time: (float) t
{
    CABasicAnimation * scaleAnimation = [CABasicAnimation animationWithKeyPath: @"transform.scale"];

    [scaleAnimation setDuration:  t ];
    [scaleAnimation setFromValue:  (id) [NSNumber numberWithDouble: s_old] ];
    [scaleAnimation setToValue:  (id) [NSNumber numberWithDouble: s_new] ];
    [scaleAnimation setTimingFunction:  [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut] ];
    [scaleAnimation setFillMode: kCAFillModeForwards];
    scaleAnimation.removedOnCompletion = NO;

    [self.contentsLayer addAnimation: scaleAnimation 
                              forKey: @"transform.scale"];

    if (self.displayShadow && self.shadowLayer)
        [self.shadowLayer addAnimation: scaleAnimation 
                                forKey: @"transform.scale"];    

    size = s_new;
}

// - - - 

- (void) resize: (float) newSize
{
    [self scaleFrom: size
                 to: newSize
               time: 1.];
}

I wonder if the problem is related to the overhead of multiple transform.scale operations queueing up -- each button resize takes a full second to complete, and if I am spinning the wheel fast I might spin a couple of revolutions per second; that means that each button is getting resized 24 times per second.

** creating the button's layer **

The final piece of the puzzle I guess is to have a look at the button's contentsLayer. but I have tried

contentsLayer.setRasterize = YES;

which should effectively be storing it as a bitmap. so with the setting the code is in effect dynamically resizing 12 bitmaps.

I can't believe this is taxing the device beyond its limits. however, core animation instrument tells me otherwise; while I am rotating the wheel (by dragging my finger in circles), it is reporting ~15fps.

This is no good: I eventually need to put a text layer inside each button and this is going to drag performance down further (...unless I am using the .setRasterize setting above, in which case it should be the same).

There must be something I'm doing wrong! but what?

EDIT: here is the code responsible for generating the button content layer (ie the shape with the shadow):

- (CALayer *) makeContentsLayer
{
    CAShapeLayer * shapeOutline = [CAShapeLayer layer];
    shapeOutline.path = self.pOutline;

    CALayer * contents = [CALayer layer];

    // get the smallest rectangle centred on (0,0) that completely contains the button
    CGRect R = CGRectIntegral(CGPathGetPathBoundingBox(self.pOutline)); 
    float xMax = MAX(abs(R.origin.x), abs(R.origin.x+R.size.width));
    float yMax = MAX(abs(R.origin.y), abs(R.origin.y+R.size.height));
    CGRect S = CGRectMake(-xMax, -yMax, 2*xMax, 2*yMax);
    contents.bounds = S; 

    contents.shouldRasterize = YES; // try NO also

    switch (technique)
    {
        case kMethodMask:
            // clip contents layer by outline (outline centered on (0, 0))
            contents.backgroundColor = self.clr; 
            contents.mask = shapeOutline;
            break;

        case kMethodComposite:
            shapeOutline.fillColor = self.clr;
            [contents addSublayer: shapeOutline];
            self.shapeLayer = shapeOutline;
            break;

        default:
            break;
    }

    if (NO)
        [self markPosition: CGPointZero
                   onLayer: contents ];

    //[self refreshTextLayer];

    //[contents addSublayer: self.shapeLayerForText];

    return contents;
}

as you can see, I'm trying every possible approach, I am trying two methods for making the shape, and separately I am toggling .shouldRasterize

** compromising the UI design to get tolerable frame rate **

EDIT: Now I have tried disabling the dynamic resizing behaviour until the wheel settles into a new position, and setting wheel.setRasterize = YES. so it is effectively spinning a single prerendered UIView (which is admittedly taking up most of the screen) underneath the finger (which it happily does @~60fps), until the wheel comes to rest, at which point it performs this laggy resizing animation (@<20fps).

while this gives a tolerable result, it seems nuts that I am having to sacrifice my UI design in such a way. I feel sure I must be doing something wrong.

EDIT: I have just tried as an experiment to resize buttons manually; ie put a display link callback in each button, and dynamically calculate the expected size of this given frame, explicitly disable animations with CATransaction the same as I did with the wheel, set the new transformation matrix (scale transform generated from the expected size). added to this I have set the buttons content layer shouldRasterize = YES. so it should be simply scaling 12 bitmaps each frame onto a single UIView which is itself rotating. amazingly this is dead slow, it is even bringing the simulator to a halt. It is definitely 10 times slower than doing it automatically using core animation's animation feature.


Are you using shadows on your layer for me it was a cause of performance issue, then you have 2 options AFAIK: - setting shadowPath that way, CA does not have to compute it everytime - removing shadows and using images to replace


I have no experience in developing iPad applications but I do have some in optimizing video games. So, I cannot give an exact answer but I want to give some tips in optimization.

Do not guess. Profile it.

It seems you are trying to make changes without profiling the code. Changing some suspicious code and crossing your fingers does not really work. You should profile your code by examining how long each task takes and how often they need to run. Try to break down your tasks and put profiling code to measure time and frequency. It's even better if you can measure how much memory are used for each task, or how many other system resources. Find your bottleneck based an evidence, not your feeling.

For your specific problem, you think the program gets really slow when your resizing work kicks in. But, are you sure? It could be something else. We don't know for sure until we have actual profiling data.

Minimize problematic area and measure real performance before making changes.

After profiling, you have a candidate for your bottleneck. If you can still split the cause to several small tasks, do it and go to profile them until you cannot split it anymore. Then, try to measure their precise performance by running them repeatedly like a few thousand times. This is important because you need to know the performance (speed & space) before making any changes so that you can compare it to future changes.

For your specific problem, if resizing is really the issue, try to examine how it performs. How long it takes to perform one resize call? How often you need to do resize work to complete your job?

Improve it. How? It depends.

Now, you have the problematic code block and its current performance. You now have to improve it. How? well, it really depends on what the problem is. you could search for better algorithms, you could do lazy fetching if you can delay calculations until you really need to perform, or you could do over-eager evaluation by caching some data if you are doing the same operation too frequently. The better you know the cause, the easier you can improve it.

For your specific problem, it might be just the limit of OS ui function. Usually, resizing button is not just resizing button itself but it also invalidates a whole area or its parent widget so that every ui stuff can be rendered properly. Resizing button could be expensive and if that's the case, you could resolve the issue by simply using image-based approach instead of OS ui-based system. You could try to use OpenGL if image operations from OS API are not enough. But, again, we don't know until we actually try them out and profile these alternatives. Good luck! :)


Try it without your shadows and see if that improves performance. I imagine it will improve it greatly. Then I'd look into using CALayer's shadowpath for rendering shadows. That will greatly improve shadow rendering performance.

Apple's Core Animation videos from last year's WWDC have a lot of great info on increasing performance in core animation.

By the way, I'm animating something way more complex then this right now and it works beautifully even on an older iPhone 3G. The hardware/software is quite capable.


You should probably just re do this in OpenGL





core-animation