App needs warmup ? First slow then smooth ?!

Hello all,

I’m working on a mobile game, it relies on drawing shapes on screen.
I have a TouchManager script that checks if the user is drawing.

  • The FIRST time the user draws, it turns out my code is super slow.
  • the SECOND time and after, it’s smooth.

I first thought that I was doing different calculations between the first and the second time, but then I decided to add a stopwatch that checks raw execution time, and the results drive me nuts …

void Update()
    {
        // ========================= START OF DRAWING =========================
          if (iSdrawing == false && Input.GetMouseButtonDown(0))
          {

            stopWatchTouchManager.Reset();
            stopWatchTouchManager.Start();

            stopWatchTouchManager.Stop();
            UnityEngine.Debug.Log("Time elapsed :" + stopWatchTouchManager.ElapsedTicks);

            //below the rest of the code

First time I run through this loop : 15 ~ 20 ticks
Second time and after : 1 ~ 2 ticks !!
I always wait a few seconds between app startup and first draw so that I make sure all initialization code has been done already.

And of course if I do the same exercice on all code that executes that if statement, I end up with ~ 15000 ticks the first time and then ~ 1500 ticks. So about a 10 times difference.

On PC it’s not really noticeable but when I test my game on a relatively low end mobile device, the game stutters for the first shape drawn which is very uncomfortable.

Any idea ?

Cheers !
Pierre

Well whats the actual code?

Maybe you are initilazing some arrays or w/e on the first run

Hey SparrowsNest,

Here it is, sorry it took me a while, had to clean it a little and comment it in English :wink:
It’s not all in there so there are parts that will seem obscure of course but anyway, the lengthy part is the one that claims “START OF DRAWING” in the Update() method. There must be many parts that can be optimized here and there, I’m a beginner coder.
Let me know if I can clarify anything.

public class TouchManager : MonoBehaviour
{
    // === Public variables used for geometric computation === \\
    public float minDistPoints;
    public float angleMax = 40f; //above this angle, we consider this is a high angle
    public float boudingSquareSize = 1f; //will be used to resize the shape
    public int resampleFactor;
    public float maxAngleForLines;
    public float minScore;

    // === Debug variables === \\
    public Text featuresInfo;
    public Text frameRateText;
    public Text shapesStatsText;
    public bool shapeEffectToggle = true;

    // === List of input points === \\
    private List<Vector3> candidatePointsRaw;
    private List<Vector3> candidatePointsTreaded;

    // === All templates the shapes aim at looking like === \\
    public List<Shape> templates = new List<Shape>();
    private WinningTemplate winningTemplate;

    // === Player / Enemy === \\
    public Enemy enemySelected;
    public Player player;
    public Transform enemyShapeTransform;

    // === Visual representation of shapes === \\
    public LineRenderer lineRenderer;
    public LineRenderer lineRendererRef;
    public GameObject trail;
    private GameObject trailReference;

    private Shape winningShapeHighlight;
    private Shape winningShapeTrail;
    public SpriteRenderer winningShapeSpriteHighlight;

    // === Coroutines === \\
    private Coroutine suspendEnemyCoroutine;
    private Coroutine moveShapeTowardsOtherCoroutine;
    private Coroutine moveShapeDownwardsCoroutine;
    private Coroutine validateShapeCoroutine;

    // === Misc variables === \\
    public GameManager gameManager;
    public Camera mainCamera;
    public Tools tool;
    private bool iSdrawing = false;
    public Animator landscapeOverlayAnimator;
    Stopwatch stopWatchTouchManager;


    void Start()
    {
        candidatePointsRaw = new List<Vector3>();

        Application.targetFrameRate = 60;
        QualitySettings.vSyncCount = 0;

        stopWatchTouchManager = new Stopwatch();
    }

    void Update()
    {
        // ============== START OF DRAWING ============== \\

        if (iSdrawing == false && Input.GetMouseButtonDown(0))
          {

            stopWatchTouchManager.Reset();
            stopWatchTouchManager.Start();

            stopWatchTouchManager.Stop();
            UnityEngine.Debug.Log("Time elapsed :" + stopWatchTouchManager.ElapsedTicks);

            iSdrawing = true;
            Vector3 mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
            mousePosition.z = 0;

            if (trailReference != null)
                Destroy(trailReference.gameObject);
            trailReference = Instantiate(trail, mousePosition, Quaternion.identity); //the trail that will follow player's touch input

            candidatePointsRaw.Clear(); //candidatePointsRaw is the List of points that are used to store raw touch input positions
            candidatePointsRaw.Add(mousePosition);

            // === Check of all coroutines === \\ If they are still running from a previous draw, we stop them to avoid weird animations lock and we destroy associated objects
            if (moveShapeTowardsOtherCoroutine != null)
                {
                    StopCoroutine(moveShapeTowardsOtherCoroutine);
 
                    if (winningTemplate.Template.staminaRequired <= player.playerCurrentStamina)
                    {
                        enemySelected.TakeDamage(winningTemplate);
                    }

                    if (winningShapeHighlight != null)
                        Destroy(winningShapeHighlight.gameObject);

                    if (lineRendererRef != null)
                        Destroy(lineRendererRef.gameObject);
                }

                if (moveShapeDownwardsCoroutine != null)
                {
                    StopCoroutine(moveShapeDownwardsCoroutine);
                    if (lineRendererRef != null)
                        Destroy(lineRendererRef.gameObject);
                }

                if (validateShapeCoroutine != null)
                {
                    StopCoroutine(validateShapeCoroutine);
                    if (lineRendererRef != null)
                        Destroy(lineRendererRef.gameObject);
                    if (winningShapeHighlight != null)
                        Destroy(winningShapeHighlight.gameObject);
                }
          
                lineRendererRef = Instantiate(lineRenderer); //The lineRenderer is used in addition to the trailRenderer to move it under specific circumstances where trailRenderer wouldn't be enough
                lineRendererRef.positionCount = 0;
                lineRendererRef.positionCount++;
                lineRendererRef.SetPosition(lineRendererRef.positionCount - 1, mousePosition);
          }


        // ============== ONGOING DRAWING ============== \\

        if (iSdrawing == true)
        {
            Vector3 mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);
            mousePosition.z = 0;

            trailReference.transform.position = mousePosition;
            float distanceLastPoint = Vector3.SqrMagnitude(mousePosition - candidatePointsRaw[candidatePointsRaw.Count - 1]);

            if (distanceLastPoint >= minDistPoints) //if current point is far enough from the last point of candidatePointRaw, we add it to the List.
            {
                candidatePointsRaw.Add(mousePosition);
                lineRendererRef.positionCount++;
                lineRendererRef.SetPosition(lineRendererRef.positionCount - 1, mousePosition);
            }
        }


        // ============== END OF DRAWING ============== \\

        if (iSdrawing == true && Input.GetMouseButtonUp(0))
        {
            iSdrawing = false;

            if (PointsOperations.PathLength(candidatePointsRaw) < 0.05f) //If the drawing is too small, we don't do anything
            {
                if(trailReference != null)
                    Destroy(trailReference.gameObject);
                if(lineRendererRef != null)
                    Destroy(lineRendererRef.gameObject);
            }
            else
            {
                // === Lookup for straight lines === \\
                if (PointsOperations.RecognizeStraightLine(candidatePointsRaw, maxAngleForLines, out winningTemplate, minScore))
                {
                    if (winningTemplate.Score > minScore) //We check the score. The score needs to be high enough. Here it is so we use the shape
                    {
                        if (winningTemplate.Template.name == "VerticalLine")
                        {
                            moveShapeTowardsOtherCoroutine = StartCoroutine(MoveShapeTowardsOther(player.transform, false)); //bool is only used when target is an enemyShape
                            validateShapeCoroutine = StartCoroutine(ValidateShape(true));
                            Destroy(lineRendererRef.gameObject); //ATTENTION maybe this needs to be kept if the vertical line needs specific animation ?
                      
                            // === Toggle between enemy suspended or not === \\
                            if (enemySelected.isEnemySuspended == false)
                            {
                                enemySelected.SuspendEnemy(true);
                                landscapeOverlayAnimator.SetBool("DisplayLandscapeOverlay", true); 
                            }
                            else
                            {
                                enemySelected.SuspendEnemy(false);
                                landscapeOverlayAnimator.SetBool("DisplayLandscapeOverlay", false);
                            }

                            gameManager.SwitchSelectorFocusOnEnemy(enemySelected); //Toggle the selector, if it was aiming NME, aim at its shapes, and vice versa
                        }
                        else //Still a straight line but not a vertical line (could be horizontal or diagonal)
                        {
                            if(winningTemplate.Template.staminaRequired <= player.playerCurrentStamina) //We check if player has enough stamina
                            {
                                player.ReduceStamina(winningTemplate.Template.staminaRequired);

                                if (enemySelected.isEnemySuspended == false)
                                    moveShapeTowardsOtherCoroutine = StartCoroutine(MoveShapeTowardsOther(enemySelected.transform, false));

                                validateShapeCoroutine = StartCoroutine(ValidateShape(true));
                            }
                            else
                            {
                                player.NotEnoughStamina();
                                validateShapeCoroutine = StartCoroutine(ValidateShape(false));
                            }
                        }
                    }
                    else //We check the score. The score needs to be high enough. Here it is NOT so we discard the shape, that's where the lineRenderer is used.
                    {
                        moveShapeDownwardsCoroutine = StartCoroutine(MoveShapeDownwards());
                    }
                }

                // === Lookup for curved lines === \\
                else
                {
                    candidatePointsTreaded = PointsOperations.DollarTreatment(candidatePointsRaw, resampleFactor, boudingSquareSize);
                    winningTemplate = PointsOperations.Recognize(candidatePointsTreaded, templates, boudingSquareSize);

                    shapesStatsText.text = PointsOperations.recognizeShapesStats; //Debug text to display scores of different templates on screen

                    if (winningTemplate.Score > minScore)  //We check the score. The score needs to be high enough. Here it is so we use the shape
                    {              
                        if (winningTemplate.Template.staminaRequired <= player.playerCurrentStamina)
                        {
                            player.ReduceStamina(winningTemplate.Template.staminaRequired);

                            if(enemySelected.isEnemySuspended == false)
                                moveShapeTowardsOtherCoroutine = StartCoroutine(MoveShapeTowardsOther(enemySelected.transform, false));

                            validateShapeCoroutine = StartCoroutine(ValidateShape(true));
                        }
                        else
                        {
                            player.NotEnoughStamina();
                            validateShapeCoroutine = StartCoroutine(ValidateShape(false));
                        }
                    }
                    else
                    {
                        moveShapeDownwardsCoroutine = StartCoroutine(MoveShapeDownwards());
                    }
                }


                // === Check if enemy is suspended === \\

                if (enemySelected.isEnemySuspended == true)
                {
                   for (int indexEnemyShape=0; indexEnemyShape < enemySelected.enemyShapesActive.Count; indexEnemyShape++)
                   {
                        if(enemySelected.enemyShapesActive[indexEnemyShape] != null) //If some EnemyShapes have not been spawn yet, we don't do anything on them
                        {
                            if (enemySelected.enemyShapesActive[indexEnemyShape].shape.name == winningTemplate.Template.name) // Check if the shape drawn by the player corresponds to one of the NME Shapes
                            {
                                if(moveShapeDownwardsCoroutine != null)
                                {
                                    StopCoroutine(moveShapeDownwardsCoroutine);
                                }

                                enemySelected.SuspendEnemy(false); //Right after an action has been positively done when NME is suspended, the suspended mode is deactived right away
                                landscapeOverlayAnimator.SetBool("DisplayLandscapeOverlay", false);
                                gameManager.SwitchSelectorFocusOnEnemy(enemySelected);

                                enemyShapeTransform.position = enemySelected.enemyShapesActive[indexEnemyShape].transform.position;

                                // === Check if the NME Shapes are in Highlight mode === \\
                                if (enemySelected.enemyShapesActive[indexEnemyShape].isHighlighted == true) //Yes : we destroy the NME Shape and it stuns the NME
                                {
                                    moveShapeTowardsOtherCoroutine = StartCoroutine(MoveShapeTowardsOther(enemySelected.enemyShapesActive[indexEnemyShape].transform, true));
                                    player.FillAllMana();
                                }
                                else //No : we break the NME Shape, it is now less powerful when attacking the player
                                {
                                    moveShapeTowardsOtherCoroutine = StartCoroutine(MoveShapeTowardsOther(enemySelected.enemyShapesActive[indexEnemyShape].transform, false));
                                }
                            }
                        }
                  
                   }
                }
            }
        }

        frameRateText.text = "\n FPS : " + 1f / Time.deltaTime;
    }
}

I took a quick look at the code, nothing screams out…
Did you try to look at the profiler to see the difference between the first execution to the rest?

I’m not really familiar with the Profiler yet, will have a look at it and will let you know, thanks for pointing that out !

Watching this, as I’ve observed similar behavior on iOS, even with all the heavy-lifting (such as GetComponent, FindObjectOfType, and Instantiate) being done in Awake/Start.

Hi again,

So I’ve managed to capture some profiling data by connecting the phone to the computer, here is what I got :


First pike is the first time I draw (within the red circle)
Other pikes are when I draw again. They are not that much smaller however they don’t make the phone stutter (while the first one really does).
Looking at the “rendering” and “memory” curves below the pikes, it looks like nothing really happened before the first pike, even tho the game has been running for a few seconds and some stuff already happen on screen. That seems like it correlates the “warmup” impression I have.

Now looking at the Hierarchy view, I have this on the first pike. Looks like Mono.JIT takes up most of the time :

This is what the second spike looks like, so no specific time around this Mono.JIT.
Although I’m confused with this one as the total Time(ms) for the Player loop is 73.02ms but the numbers right below don’t add up to 73.02ms (maybe it’s just that I don’t really get how to read this).

Anyway, any idea what this Mono.JIT could be ?
Is this related to memory allocation that make everything slower, and once it’s done once, then no need to redo it again so the app is smooth ?

Let me know if I can provide more info from this profiling data.

Thanks !

Not sure what follows where the screenshot is cut off, but it usually adds up.

Every “parent node”, usually a routine that does something + calls other routines, shows the total time of itself + all the listed child nodes. So if you expand a node, you’ll see how the expanded node’s total time splits among its child nodes, so on and so forth.

PlayerLoop takes 73.02 ms in total.
46.65 ms / 73.02 ms belong to Update.ScriptRunBehaviourUpdate
15.01 ms / 73.02 belong to PostLateUpdate.PlayerUpdateCanvases (at the bottom)

There’s probably more down there where it’s cut off, we can’t see that…
The remaining seconds are taken by the PlayerLoop routine itself.

It’s the runtime’s Just In Time compiler.

My guess is that it has to “jit” (i.e. compile just in time) some of the instructions when they’re needed for the first time. Sometimes this can take relatively long. The results are probably cached for later efficient re-use, which might explain why you only have those spikes the first time it’s executed.

I have to admit I’m not a pro in JIT and AOT compilation stuff.
But I’m sure @lordofduct can share some insights.

1 Like

Thanks for the info Suddoha !
It does look like a cache thing, well spotted ! Would there be a way to “cache” everything I need in a Start() function ?

Also when I said I was confused, you can see it in the screenshot below.
Take the “parent” TouchManager.Update() : 46.27ms total.
If you add all the children, it’s all tiny numbers that certainly don’t add up to 46.27ms.
The next “parent” will be 15.01ms PostLateUpdate.PlayerUpdateCanvases

I cannot give a definite answer to that.

If the premise were correct (JIT compiler jitting the instructions for the first time), you could certainly attempt to have the code jitted prior to the first user-triggered execution. You could check whether an attempt to let your application “fake” the first drawing helps to fix this temporarily (for instance when you’re about to hit a deadline, or have more important stuff to deal with). Kind of a warm-up execution. You’d undo / reset at the end of the frame, or in the subsequent frame.

Temporarily, because we still do not know whether we’ll get this sorted.
I wouldn’t take it as the final solution though.

Refactoring:
What stands out is that you’ve got a very long Update method. And it contains deeply nested control structures, i.e. lots of ifs and loops. It could be worse, but there’s lot’s of potential to reduce redundancy, as well as improve readability and perhaps even overall efficiency. Your touch manager does most-likely have too much responsibilities.

The first thing I’d do: split the Update method into multiple methods. This can have the very beneficial side-effect that you start working towards self-documenting source code.
And it will be easier to identify whether a functionality belongs to the TouchManager, since you have to find concise names for the methods.

Unrelated to the JIT issue, there are alarming numbers of allocations in your TouchManager. Round about 1400 which allocate 0.3 mega bytes and swallow 2 ms.

Not sure which part causes those (I haven’t looked through the entire script), but you should attempt to avoid that much allocations. If it’s coming from a list that keeps resizing its internal array, you can force it to pre-allocate enough capacity.

Like I stated earlier, the total time for a given “parent” is the time it took for the code associated with the parent itself + all the children, or “self” + children. Check out the column just next to the one in which you looked up the time, that should be the time you’ve missed.

By the way. You can also enable “deep profiling”. It may run much slower, but you get even more insight into your code’s resource and time statistics.

Thanks for taking the time to dig !

So I’ve tried to “simulate” a first draw. This is very ugly but I basically copy pasted my whole Update loop in the Start function, just for the purpose of the tests. I simulated user input and instead of using Input.mousePosition, I used a random vector. Well, it unfortunately didn’t do the trick :frowning:
Maybe this is related to the Input class ? The first time it’s being called, it does some computations maybe ? I guess it could be fixed by forcing the player to press a button before the beginning of every game, but not really clean.
Actually once I have my whole game structure, the game will start with menus. Maybe this will initialize the whole Input functions once and for all and the problem will be solved by that.

As for the other points you suggest, they are very valid, thanks for tips.
The Update function is indeed massive. The multiple calls you point are probably due to the lines 215 and 216 :

candidatePointsTreaded = PointsOperations.DollarTreatment(candidatePointsRaw, resampleFactor, boudingSquareSize);
                    winningTemplate = PointsOperations.Recognize(candidatePointsTreaded, templates, boudingSquareSize);

Basically they are related to recognition algorithm and yes, they use extensive amounts of lists.
I will check how fixed allocation could improve this.

Will keep investigating. Thanks again !

The result of copy&paste is that you’re having just another function with all that stuff that needs to be jitted. They’re not the same and thus cannot be jitted just once.

Instead, if you were to move the code to a method, and call it from anywhere. That particular method could probably be jitted on it’s first call, and could be cached for re-use afterwards.

Basically, what I mean is, the JIT compiler could treat those differently:

// JIT this one with all its instructions and cache it
void Start()
{
     // my code
}

// JIT this one as well with everything it contains, and cache it
void Update
{
     // my code, just like above
}

and

// not alot to jit here
void Start()
{
    MethodWithAllTheCode();
}

// not alot to jit here
void Update()
{
   MethodWithAllTheCode();
}

// JIT this once and cache it
void MethodWithAllTheCode()
{
    // my code
}

Don’t quote me on that, I’m just trying to help out as long as noone else posts here :p. But for now, I would assume this yields different results with regards to jitting.

Not sure if that makes any sense. Looking forward to see your reports. :slight_smile:

So I’m out of town currently in Seattle and only have my tablet (and therefore typing/mousing suuuuucks). So I can only go so far with discussion.

So yeah, the slow down is the JIT compiler as suddoha pointed out. Looking at the code you posted it’s a fairly large method that also branches into several other method calls of code that I don’t see… but I’m going to bet is not trivial. (like PointOperations)

So suddoha is talking about pre-jitting your code instead of waiting for the first time it gets called. And this would likely help out your issues. There are actually helper methods for this… though I can’t vouch for their implimentation in the version of mono used by Unity (or which version of mono you’re targetting). But this is defintiely something you can toy with and test out. See what bennies it gets you with your build/target version.

So the main method you’ll be interested in is RuntimeHelpers.PrepareMethod:

And actually there are some different articles out there about creating an entry point for easily attributing methods you want to prejit. Here’s one that you might find interesting:
https://www.codeproject.com/Articles/34148/JIT-methods-at-runtime

This actually is something I think could be interseting in the Unity world and I’m surprised I haven’t looked into it before now in the unity context. But unfortunately due to the limitations of my current setup I can’t really go deeper into it at the moment with you. Be home in a couple days though and I’ll check back in on this thread.

You’re in good hands with suddoha in the mean time though.

3 Likes

Nice ! I will have a deeper look at this.

In the meantime, I’ve tried your method Suddoha. I put everything in one single method that I call once during the Start() function and then it gets called every update :

void Start()
    {
        stopWatchTouchManager = new Stopwatch();

        stopWatchTouchManager.Reset();
        stopWatchTouchManager.Start();

        DrawAndRegognize();

        stopWatchTouchManager.Stop();
        UnityEngine.Debug.Log("First time the function is called : " + stopWatchTouchManager.ElapsedTicks + " ticks");
    }

    void Update()
    {
        stopWatchTouchManager.Reset();
        stopWatchTouchManager.Start();

        DrawAndRegognize();

        stopWatchTouchManager.Stop();
        UnityEngine.Debug.Log("Second time and more : " + stopWatchTouchManager.ElapsedTicks + " ticks");
}

Results :
4808051--460124--upload_2019-7-31_20-5-56.png

This works like a charm :slight_smile: … in the Editor.
Back to my mobile, I still get the noticeable lag for my first input :frowning:

So I made another test. Super simple app, just a button that changes the color of the camera background when pressed.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ButtonActions : MonoBehaviour
{
    public void ChangeColor()
    {
        GetComponent<Camera>().backgroundColor = new Color(Random.Range(0f, 1f), Random.Range(0f, 1f), Random.Range(0f, 1f));
    }
}

Here is the result :

Then after than I made an even simple button app. There is just a button, and it just does nothing :

Seems like a JIT thing again… So the first time the app listens to an input, a big lag happens, at least on this phone (Motorola Moto G running Android 5, bought in 2013 for less than 150 euros new … yep, entry level, but again I’m developping a game that’s supposed to run on this kind of phones, and other similar games run smoothly on this).

Do you think this input can be “prepared” in some way ? Or I will just have to admit the fact that unity doesn’t run that well on very older phones ?

Edit : Expanding the Mono.JIT in the last screenshot, here’s what’s under :

If you’re doing this on a phone, you could go with the IL2CPP backend. That’s AOT-based, which means no JIT:

I need to go to bed now, so please google those terms.

1 Like

Wow man, that seems to do the trick ! No more input lag at the beginning and the game even seems to runs more smoothly in general, it’s awesome thanks !!
Why isn’t this the default building mode ?

For those who would like to know how to change this, it’s in there (Build Settings > Player Settings)

2 Likes

Because it takes for fucking ever, compared to non-il2cpp. IL2CPP means "take the IL (the compiled C# code), and convert it to C++. Then, compile that C++. Both the c++ generation and compilation takes a lot of time.

Not having a JIT also causes a bunch of issues, as C# was designed with a JIT in mind. When running AOT, the only classes you can use are the ones defined in your program text. The JIT, on the other hand, just compiles those types when they show up.

And you’d be surprised by how common it is to use classes that’s not directly mentioned in your program’s text. That’s usually a side-effect of using generics. So on AOT platforms, you might get crashes due to that. You can work around it, but it’s still annoying.

@alexeyzakharov , It’s a bit hard to tell from the screenshots in this thread what method is being jit-compiled. I’m guessing that it’s the method that it’s directly under, but it could be something else due to the profiler not being in deep profile mode, no? Maybe, for clarity, you could change the name of “Mono.JIT” in the profiler to “Mono.JIT compiling type X” ?

1 Like

Just be aware, that everything inside your called method has be JIT compiled.
Including scripts on the instantiated objects.

Also, coroutines. They’re not JIT compiled until they’re accessed / run.

To negate some of the performance impacts of JIT, try to keep your methods as simple as possible.
Split your methods into multiple ones by logic they should do instead of one big sausage.