2

Background: I have a game that i've been primarily testing on a (lowish spec) laptop. It runs fine. When testing on the xbox there is one method that appears to be seriously effecting performance/fps when it gets called. On the PC you wouldn't notice any slowdown/skipped frames.

I've profiled on the xbox and on average i get a GC about 1-2 times a second, taking 20-40ms when the game is running.

I've noticed no change in GC rate or duration when my slow method is running.

Next i tried profiling on the PC to identify what in the method was taking the most time. Turns out it was doing List<T>.Contains(), so i created my own class that had a List<T> and a HashSet<T> internally, so i could use the HashSet<T> internally for Contains().

I've reached the point now, where i can't really think of what else to tune without changing the algorithm, and i think the algorithm is as simple as it's going to get.

I know i can't profile to get method times / percentages on the xbox, so i'm at a bit of a loss as to what to try next.

I've included the code below which is used to simulate flowing water in a tile-based system. It runs once per water tile, trying to move it downwards possibly through other water tiles (generally speaking).

Question: I want to know if i'm doing anything obviously wrong (i.e. would impact xbox performance badly) here. Is calling Funcs slow? Am i getting boxing anywhere with my Point objects? etc.

Appologies for the vast amount of code! The tiles themselves come from an object pool to minimise GC. The InternalThink() method is what's causing all the issues.

public abstract class TileFlowing : TileMovable
{
    private FastList<Point> TeleportSwapLocations = new FastList<Point>();

    private FastList<Point> PossibleMoveLocations = new FastList<Point>();

    private FastQueue<Point> PositionsToCheck = new FastQueue<Point>();

    private FastList<Point> PositionsChecked = new FastList<Point>();

    private static Comparison<Point> _PossibleMoveComparer;

    public bool Static = false;

    protected abstract Func<Point, int> PossibleMoveLocationOrdering { get; }

    protected abstract Func<Point, Point, bool, bool> MoveSidewaysFunc { get; }

    protected abstract int MaxUnitsWithin { get; }

    protected abstract int Weight { get; }

    public int UnitsWithin;

    public int AvailableToFlowThrough = 0;

    protected virtual bool RecurseTilesUp { get { return true; } }
    protected virtual bool RecurseTilesDown { get { return true; } }
    protected virtual bool RecurseTilesLeft { get { return true; } }
    protected virtual bool RecurseTilesRight { get { return true; } }

    public TileFlowing()
        : base()
    {

    }

    public override void LoadContent(Components.TileGridManagement.GameGrid Owner)
    {
        base.LoadContent(Owner);

        _PossibleMoveComparer = (Point P1, Point P2) =>
        {
            int Result = PossibleMoveLocationOrdering(P1) -
                            PossibleMoveLocationOrdering(P2);
            if (Result == 0)
                Result = (IsSameType(P1) ? (_Owner[P1] as TileFlowing).UnitsWithin : 0) -
                            (IsSameType(P2) ? (_Owner[P2] as TileFlowing).UnitsWithin : 0);

            return Result;
        };
    }

    public override void ResetProperties()
    {
        base.ResetProperties();

        Static = false;
        UnitsWithin = MaxUnitsWithin;
        AvailableToFlowThrough = 0;
    }

    public override void CopyProperties(Tile SourceTile)
    {
        base.CopyProperties(SourceTile);

        Static = (SourceTile as TileFlowing).Static;
        UnitsWithin = (SourceTile as TileFlowing).UnitsWithin;
        AvailableToFlowThrough = (SourceTile as TileFlowing).AvailableToFlowThrough;
    }

    public override void Think()
    {
        base.Think();

        InternalThink(false, false);
    }

    public override void FactoryThink()
    {
        base.FactoryThink();

        InternalThink(true, false);
    }

    public void FlowThink(bool CalledFromFactoryThink)
    {
        InternalThink(CalledFromFactoryThink, true);
    }

    private bool IsSameType(Point Position)
    {
        return IsSameType(Position.X, Position.Y);
    }

    private bool IsSameType(int X, int Y)
    {
        return _Owner[X, Y] != null && _Owner[X, Y].GetType() == GetType();
    }

    private bool IsDifferentFlowingTile(Point Position)
    {
        return IsDifferentFlowingTile(Position.X, Position.Y);
    }

    private bool IsDifferentFlowingTile(int X, int Y)
    {
        return !IsSameType(X, Y) && _Owner[X, Y] is TileFlowing;
    }

    protected void CheckPosition(Point PositionToCheck, Point TilePosition, bool CalledFromFactoryThink, bool CalledFromFlowThink,
                                 ref FastList<Point> PossibleMoveLocations, ref FastList<Point> TeleportSwapLocations, ref FastQueue<Point> PositionsToCheck,
                                 Func<Point, Point, bool, bool> ClearCheckFunc)
    {
        if (IsSameType(PositionToCheck))
        {
            if (!PositionsToCheck.Contains(PositionToCheck))
                PositionsToCheck.Enqueue(PositionToCheck);
        }
        else if (_Owner[PositionToCheck] is TileFlowing && (ClearCheckFunc == null || ClearCheckFunc(PositionToCheck, TilePosition, CalledFromFactoryThink)))
        {
            // If we weigh more than the other tile, or we're called from the factory think (are under pressure)
            if ((_Owner[PositionToCheck] as TileFlowing).Weight < Weight || CalledFromFactoryThink)
            {
                if (!(_Owner[PositionToCheck] as TileFlowing).Static || !CalledFromFlowThink)
                    PossibleMoveLocations.Add(PositionToCheck);
            }
        }
        else if (_Owner.IsClear(PositionToCheck) && (ClearCheckFunc == null || ClearCheckFunc(PositionToCheck, TilePosition, CalledFromFactoryThink)))
        {
            PossibleMoveLocations.Add(PositionToCheck);
        }
    }

    private int PossibleMoveLocationsComparer(Point P1, Point P2)
    {
        return (PossibleMoveLocationOrdering(P1) - PossibleMoveLocationOrdering(P2)) * 1000 +
               ((IsSameType(P1) ? (_Owner[P1] as TileFlowing).UnitsWithin : 0) - (IsSameType(P2) ? (_Owner[P2] as TileFlowing).UnitsWithin : 0)) * 100;
    }

    protected void InternalThink(bool CalledFromFactoryThink, bool CalledFromFlowThink)
    {
        AvailableToFlowThrough = 0;

        TeleportSwapLocations.Clear();

        PossibleMoveLocations.Clear();

        PositionsToCheck.Clear();

        PositionsChecked.Clear();

        PositionsToCheck.Enqueue(Position);

        while (PositionsToCheck.Count != 0)
        {
            Point PositionToCheck = PositionsToCheck.Dequeue();

            if (!PositionsChecked.Contains(PositionToCheck) &&
                ((_Owner[PositionToCheck] as TileFlowing).AvailableToFlowThrough < MaxUnitsWithin || CalledFromFactoryThink))
            {
                if (((_Owner[PositionToCheck] as TileFlowing).Static && !CalledFromFactoryThink))
                    continue;

                if (PositionToCheck != Position)
                {
                    (_Owner[PositionToCheck] as TileFlowing).AvailableToFlowThrough++;
                }

                PositionsChecked.Add(PositionToCheck);

                if ((_Owner[PositionToCheck] as TileFlowing).UnitsWithin < MaxUnitsWithin && PositionToCheck != Position)
                {
                    PossibleMoveLocations.Add(PositionToCheck);

                    if (CalledFromFactoryThink && (_Owner[PositionToCheck] as TileFlowing).UnitsWithin + UnitsWithin <= MaxUnitsWithin)
                        continue;
                }

                // Check below
                Point PosBelow = new Point(PositionToCheck.X + TileDirection.Down.X, PositionToCheck.Y + TileDirection.Down.Y);

                CheckPosition(PosBelow, Position, CalledFromFactoryThink, CalledFromFlowThink, ref PossibleMoveLocations, ref TeleportSwapLocations, ref PositionsToCheck, null);

                // Check one horizontal direction
                Point RandHDir = Randomiser.GetHDirection();

                Point RandHPos = new Point(RandHDir.X + PositionToCheck.X, RandHDir.Y + PositionToCheck.Y);

                CheckPosition(RandHPos, Position, CalledFromFactoryThink, CalledFromFlowThink, ref PossibleMoveLocations, ref TeleportSwapLocations, ref PositionsToCheck, MoveSidewaysFunc);

                // Check the other horizontal direction
                Point OtherHDir = new Point(-RandHDir.X, RandHDir.Y);

                Point OtherHPos = new Point(OtherHDir.X + PositionToCheck.X, OtherHDir.Y + PositionToCheck.Y);

                CheckPosition(OtherHPos, Position, CalledFromFactoryThink, CalledFromFlowThink, ref PossibleMoveLocations, ref TeleportSwapLocations, ref PositionsToCheck, MoveSidewaysFunc);

                // Check above if appropriate
                Point AbovePos = new Point(PositionToCheck.X + TileDirection.Up.X, PositionToCheck.Y + TileDirection.Up.Y);

                if (TileDirection.Below(AbovePos, Position) || CalledFromFactoryThink)
                {
                    CheckPosition(AbovePos, Position, CalledFromFactoryThink, CalledFromFlowThink, ref PossibleMoveLocations, ref TeleportSwapLocations, ref PositionsToCheck, null);
                }
            }
        }

        PossibleMoveLocations.Sort(_PossibleMoveComparer);

        bool Moved = false;

        if (PossibleMoveLocations.Count != 0)
        {
            if (CalledFromFactoryThink)
            {
                while (UnitsWithin != 0 && PossibleMoveLocations.Count != 0)
                {
                    int OldUnitsWithin = UnitsWithin;

                    Moved = IterateTeleport(CalledFromFactoryThink, ref PossibleMoveLocations, (P) => !IsDifferentFlowingTile(P), Moved);

                    if (UnitsWithin == OldUnitsWithin)
                    {
                        Moved = IterateTeleport(CalledFromFactoryThink, ref PossibleMoveLocations, (P) => IsDifferentFlowingTile(P), Moved);
                    }

                    PossibleMoveLocations.RemoveAll(P => IsSameType(P) && (_Owner[P] as TileFlowing).UnitsWithin == MaxUnitsWithin);
                }
            }
            else
            {
                Moved = Moved || Teleport(PossibleMoveLocations[0]);
            }

            // If we did move and not because we were forced to then mark all mercury tiles above and left or right as not static.
            if (!CalledFromFactoryThink)
            {
                _Owner.RecurseTiles(Position,
                                    (P) => RecurseTilesUp && IsSameType(P.X + TileDirection.Up.X, P.Y + TileDirection.Up.Y),
                                    (P) => RecurseTilesDown && IsSameType(P.X + TileDirection.Down.X, P.Y + TileDirection.Down.Y),
                                    (P) => RecurseTilesLeft && IsSameType(P.X + TileDirection.Left.X, P.Y + TileDirection.Left.Y),
                                    (P) => RecurseTilesRight && IsSameType(P.X + TileDirection.Right.X, P.Y + TileDirection.Right.Y),
                                    (P) =>
                                    {
                                        if (IsSameType(P))
                                            (_Owner[P] as TileFlowing).Static = false;
                                    });
            }
        }
        else
        {
            // Mark this tile as static if we didn't move, no water moved through this tile and we have all the units we can take.
            Static = (AvailableToFlowThrough == 0) && (UnitsWithin == MaxUnitsWithin); // TODO: 9 Fix flowing tiles becoming static and getting stuck
        }

        if (!Moved)
        {
            // If we haven't moved
            if (TeleportSwapLocations.Count != 0)
            {
                Moved = TeleportSwap(TeleportSwapLocations[0]);
            }

            if(!Moved)
            {
                // If we didn't move, undo checked tiles
                foreach (var CheckedPosition in PositionsChecked)
                {
                    (_Owner[CheckedPosition] as TileFlowing).AvailableToFlowThrough--;
                }
            }
        }
    }

    private bool IterateTeleport(bool CalledFromFactoryThink, ref FastList<Point> SortedPossibleMoveLocations, Func<Point, bool> PossibleMoveLocationsFilter, bool Moved)
    {
        foreach (var PossibleMoveLocation in SortedPossibleMoveLocations)
        {
            if (PossibleMoveLocationsFilter(PossibleMoveLocation))
            {
                if (IsDifferentFlowingTile(PossibleMoveLocation))
                {
                    bool OldStatic = Static;

                    Static = true;
                    (_Owner[PossibleMoveLocation] as TileFlowing).FlowThink(CalledFromFactoryThink);
                    Static = OldStatic;
                }

                bool TeleportResult = Teleport(PossibleMoveLocation);
                Moved = Moved || TeleportResult;

                if (TeleportResult)
                    break;
            }
        }
        return Moved;
    }

    protected bool TeleportSwap(Point NewPosition)
    {
        TileFlowing OurNewTile = (TileFlowing)Tile.GetNewTileFromStore(this);
        OurNewTile.CopyProperties(this);
        OurNewTile.Position = NewPosition;

        TileFlowing ReplacedTile = (TileFlowing)Tile.GetNewTileFromStore(_Owner[NewPosition]);
        ReplacedTile.CopyProperties(_Owner[NewPosition]);
        ReplacedTile.Position = Position;

        _Owner.ClearTile(NewPosition);
        _Owner.AddTileToGrid(OurNewTile);

        _Owner.ClearTile(Position);
        _Owner.AddTileToGrid(ReplacedTile);

        UnitsWithin = 0;

        return true;
    }

    protected bool Teleport(Point NewPosition)
    {
        if (IsDifferentFlowingTile(NewPosition))
        {
            return TeleportSwap(NewPosition);
        }
        else
        {

            TileFlowing NewTile;

            bool RemovedAllUnits = false;

            int NewPositionUnits = IsSameType(NewPosition) ? (_Owner[NewPosition] as TileFlowing).UnitsWithin : 0;

            int UnitsToRemove = Math.Min(UnitsWithin,
                                         Math.Max(1,
                                                  Math.Min(Math.Abs(UnitsWithin - NewPositionUnits) / 2,
                                                           MaxUnitsWithin - NewPositionUnits)));

            UnitsWithin -= UnitsToRemove;

            if (IsSameType(NewPosition))
            {
                (_Owner[NewPosition] as TileFlowing).UnitsWithin += UnitsToRemove;
            }
            else
            {
                NewTile = (TileFlowing)Tile.GetNewTileFromStore(this);

                NewTile.Position = NewPosition;
                NewTile.UnitsWithin = UnitsToRemove;

                _Owner.AddTileToGrid(NewTile);
            }

            if (UnitsWithin == 0)
            {
                _Owner.ClearTile(Position);
                RemovedAllUnits = true;
            }

            return RemovedAllUnits;
        }
    }
}
George Duckett
  • 31,770
  • 9
  • 95
  • 162
  • @Marc: Looking at msdn, it appears not (don't know what that means though). – George Duckett Jun 14 '11 at 07:38
  • @Marc: Scratch that above comment, i was looking here, http://msdn.microsoft.com/en-us/library/ms132136.aspx which doesn't have the xbox icon, but here http://msdn.microsoft.com/en-us/library/ms132123.aspx it does. So i'm not sure now, but looks like it does support it. – George Duckett Jun 14 '11 at 07:40
  • *"I know i can't profile to get method times / percentages on the xbox"* - have you checked out the Eqatec profiler? - http://www.eqatec.com/Profiler/ – MattDavey Jun 14 '11 at 08:39
  • @MattDavey. I did not know about that! Will definately take a look when i'm next infront of my xbox. – George Duckett Jun 14 '11 at 09:24
  • @George Duckett - XBox support used to be one of the big selling points of the Eqatec profiler, but they seem to have removed all mention of it from their website (pushing winphone support instead). I hope it's still Xbox compatible :) – MattDavey Jun 14 '11 at 13:29
  • When you say that you've noticed no change in the GC rate or duration when your slow method is running, do you mean that you've measured the garbage collections with your slow method being called normally and you've measured them without your slow method being called at all and they are the same? – Venesectrix Jun 16 '11 at 01:20
  • @Venesectrix: I've observed GCs without the slow method being called at all, and with the slow method being called. – George Duckett Jun 16 '11 at 07:47
  • I'd understand if it was slow on my laptop, in that case i'd just have a method that was obviously called too much or was doing to much, but the fact that it's only slow on the xbox makes me suspicious. – George Duckett Jun 16 '11 at 07:48
  • I see you using a lot of lambdas in the InternalThink function. http://stackoverflow.com/questions/1582754/does-using-a-delegate-create-garbage (and other references on the web) will tell you that anonymous functions create garbage that must be collected. Have you tried replacing the generation of anonymous functions with functions you can reuse? – Venesectrix Jun 16 '11 at 16:11
  • @Venesectrix, thanks for the idea, i hadn't thought of that. I'll go through and apply that to the rest of my game too, however, it's not helped in this case. I've noticed no differnce in GC calls (GC calls don't increase while this method is running either). – George Duckett Jun 16 '11 at 18:46
  • @MattDavey Don't worry: the EQATEC Profiler is still as XBox-capable as ever - no functionality has been removed. – Richard Flamsholt Jun 18 '11 at 20:45
  • @Richard. I've downloaded the free version - i can't find anything about how to deploy and test on the xbox. Is there any help regarding building a profiling version of an XNA dll, deploying it to the xbox, and profiling it? I can't find any help regarding this anywhere at all. – George Duckett Jun 18 '11 at 23:39
  • @George You're right, there's no help for that. Frankly we (eqatec) have never tried it on an XBox and that's why I said that it was as "capable as ever": if it had worked before for someone, as it apparently had, then it will work just as fine now. Maybe Matt can tell you more? – Richard Flamsholt Jun 20 '11 at 13:08

2 Answers2

0

You can use a Stopwatch to do some basic profiling, I think. And you could consider setting a millisecond or iteration 'limit' on the think method - so it won't ever take more than 5ms / 200 iterations or whatever works.

Kieren Johnstone
  • 41,277
  • 16
  • 94
  • 144
  • Thanks, it does look like it's going to come down to just bog standard profiling. I also like your limit idea, although that's a last resort really. I'm hoping i'm doing something silly performance-wise. – George Duckett Jun 14 '11 at 07:48
  • It depends on the amount of adjacent water tiles (flood-fill-ish algorithm), but roughly one iteration per tile. About 30 iterations per tile in my test case. – George Duckett Jun 14 '11 at 08:54
  • The thing that gets me is that the number of iterations its doing is the same on xbox and (lower powered) pc, yet the xbox is orders of magnitude slower. So i don't think it's just that i'm doing a lot of calculations (since that would slow down the pc too). – George Duckett Jun 14 '11 at 08:57
  • How many tiles then? What I mean, is, how many times is this code being looped through :) – Kieren Johnstone Jun 14 '11 at 09:26
  • Sorry, should've been clearer, about 30 * 30 times per frame. – George Duckett Jun 14 '11 at 09:30
  • Is that it? It's very very very unlikely your problem is here, if it's only the code included. Edit: added another very – Kieren Johnstone Jun 14 '11 at 10:07
  • Hmm, i'll edit the code to count total iterations rather than manually extrapolating based on one measurement.... – George Duckett Jun 14 '11 at 10:08
  • I've double-checked and it is only running roughly that number of times. I've profiled on the PC and when water is moving, that method is the one using the most CPU time. – George Duckett Jun 14 '11 at 10:57
  • How much time is it using, as a percentage of total or per second/per frame? It might be using the most time but something has to, if you get my meaning :) – Kieren Johnstone Jun 14 '11 at 12:01
  • On my laptop the Update function is 8% of the frame time (the rest is drawing, which is slow because of graphics card), my `InternalThink` function is 6% of frame time. So, 75% of the total is the `Internal Think` function. **Note:** Although this profiling is on the PC, tests on the xbox confirm that when i know the method won't be running gameplay is smooth, however when i know it will be called gameplay is jerky. – George Duckett Jun 14 '11 at 13:13
  • P.s. Appreciate your patience! I'm still hoping this question uncovers some *do not's* for xbox coding. – George Duckett Jun 14 '11 at 13:15
0

Just a wild guess here, but...

This repeated lookup and cast in the while loop looks a bit dodgy to me: _Owner[PositionToCheck] as TileFlowing. I would be tempted to pull it out as a variable and see what happens.

ngoozeff
  • 4,576
  • 3
  • 28
  • 21