Quantcast
Channel: winJade » Infinite Loop
Viewing all articles
Browse latest Browse all 2

A lesson on infinite loops

$
0
0

 quality assurance baby

 

Yesterday, I took a look at the varying perspectives taken with regards to the Zune 30 debacle. Today, I’ll take a look at what exactly led the Zune 30s to freeze. If you’d like to see the code for the entire driver, click here.

Below the fold lies a sufficiently sized code sample with everything you’ll need to understand what happened with the Zune 30 bug.


Relevant code is non-gray code:

#define ORIGINYEAR 1980 
BOOL ConvertDays(UINT32 days, SYSTEMTIME* lpTime)
{
    int dayofweek, month, year;
    UINT8 *month_tab;

    //Calculate current day of the week
    dayofweek = GetDayOfWeek(days);

    year = ORIGINYEAR;

    while (days > 365)
    {
        if (IsLeapYear(year))
        {
            if (days > 366)
            {
                days -= 366;
                year += 1;
            }
        }
        else
        {
            days -= 365;
            year += 1;
        }
    }

    // Determine whether it is a leap year
    month_tab = (UINT8 *)((IsLeapYear(year))? monthtable_leap : monthtable);

    for (month=0; month<12; month++)
    {
        if (days <= month_tab[month])
            break;
        days -= month_tab[month];
    }

    month += 1;

    lpTime->wDay = days;
    lpTime->wDayOfWeek = dayofweek;
    lpTime->wMonth = month;
    lpTime->wYear = year;

    return TRUE;
}

So what’s the purpose of the while loop? To put it simply, it’s designed to get the number of years from the number of days since 1980 as well as a remainder of days out of the current year. Well, let’s look at how the code plays out in my confusingly simple While Loop in Plain English™. Keep in mind that you’re never supposed to retry the loop without committing some sort of action which will ultimately exit the loop:

  1. year is set to ORIGINYEAR. Since ORIGINYEAR is 1980, any addition to year is added on top of 1980. This is fine because the hardware is passing the number of days since January 1, 1980.
  2. Is the number of days greater than 365? If so, proceed. Otherwise, skip to number 6.
  3. Is the current year a leap year? If so, proceed. Otherwise, subtract 365 from the number of days, add 1 to the number of years, and skip to number 5.
  4. Is the number of days greater than 366? If so, subtract 366 from the number of days, add 1 to the number of years, and proceed.
  5. retry number 2.
  6. Profit?

The problem lies in the fact that there is one case which can never escape the loop. If it’s a leap year, the number 366 will stay within the loop forever because 366 will never be greater than 366, but 366 will also always be greater than 365. 366 days will pass through, in our Plain English While Loop, without having any action committed upon it.

There’s a very quick solution to this: Since 366 is a valid remainder of days in a leap year (December 31 is the 366th day in a leap year), 366 can technically exit the loop without any problems. All we need to do is add a way to exit the loop when the number of days is 366. Let’s see how things go with this new breakpoint:

  1. year is set to ORIGINYEAR. Since ORIGINYEAR is 1980, any addition to year is added on top of 1980. This is fine because the hardware is passing the number of days since January 1, 1980.
  2. Is the number of days greater than 365? If so, proceed. Otherwise, skip to number 6.
  3. Is the current year a leap year? If so, proceed. Otherwise, subtract 365 from the number of days, add 1 to the number of years, and skip to number 5.
  4. Is the number of days greater than 366? If so, subtract 366 from the number of days, add 1 to the number of years, and proceed. Otherwise, skip to number 6.
  5. retry number 2.
  6. Profit!

Thanks to the break (represented by “Otherwise, skip to number 6”), 366 days can escape the loop properly. This would lead to the end result for December 31, 2008 being years == 2008 and days == 366.

This is what the break would look in the while loop:

    while (days > 365)
    {
        if (IsLeapYear(year))
        {
            if (days > 366)
            {
                days -= 366;
                year += 1;
            }
            else
            {
                break;
            }
        }
        else
        {
            days -= 365;
            year += 1;
        }
    }

Clarifying one quick thing: A number of people on outlets such as Digg suggested that changing “if (days > 366)” to “if (days >= 366)”:

QAdigg

The problem with doing this is that the end result, upon hitting “if (days >= 366)” with the value 366, one would end up with the year going up by one and the operation “366 –= 366” playing out. End result: December 31 of the leap year is suddenly January 0 of the following year.

 

Extra credit: This loop can be fixed such that the loop’s condition is based on the year’s status as a leap year. The change needed is simple:

    while (days > (IsLeapYear(year) ? 366 : 365))
    //The above line basically says “Is it a leap year? Check days against 366.
    //Otherwise, check it against 365”
    {
        if (IsLeapYear(year))
        {
            days -= 366;
            year += 1;
            /*You’ll notice we no longer need a break. The loop condition now
             *checks the days properly. Because the loop properly checks
             *whether days is greater than 365 or 366, the second days > 366
             *check is no longer needed. */
        }
        else
        {
            days -= 365;
            year += 1;
        }
    }

 

However, if this change is made, IsLeapYear() is now being called twice. This is inefficient, especially on small devices (like the devices which would be using this driver). In order to reduce the number of times IsLeapYear() is being called, it would be best to store the value of IsLeapYear() in a variable.

    int leap = IsLeapYear(year);
    //leap is determined before the loop begins.
    //In this case, it would be true (1980 is a leap year)
    while (days > (leap ? 366 : 365))
    {
        if (leap)
        {
            days -= 366;
            year += 1;
        }
        else
        {
            days -= 365;
            year += 1;
        }
        leap = IsLeapYear(year);
        //leap is determined again now that the year has changed.
    }

This can actually be reduced even further. If you look at how the if/else statement is operating, you can see that it’s basically saying “If it’s a leap year, subtract 366. Otherwise, subtract 365,” which is similar to the while loop’s condition!

    int leap = IsLeapYear(year);
    while (days > (leap ? 366 : 365))
    {
        days -= (leap ? 366 : 365);
        //days is equal to days - 366 if it's a leap year,
        //                 days - 365 if it isn't.
        year += 1;
        leap = IsLeapYear(year);
    }

If you wanted to reduce the number of times (leap ? 366 : 365) is checked, you could do this instead:

    int daysThisYear = (IsLeapYear(year) ? 366 : 365);
    //The number of days in the current year is now calculated instead. 
    while (days > daysThisYear)
    {
        days -= daysThisYear;
        year += 1;
        daysThisYear = (IsLeapYear(year) ? 366 : 365);
    }

It’s questionable whether or not this code is any better than the original solution with the break, but this would be the “proper” way to do it (instead of using an arbitrary break in your code). It’s also easier to understand if you know C.

The irony in all of this is that this specific driver is used in multiple Windows CE devices, not just the Zune. This is why quality assurance is so necessary.

There are three morals here:

  1. Test every condition which could happen in every single loop in your code (though, as Matt Boehm points out in the comments, this might be a tad bit hard).
  2. Don’t visit Digg for programming advice. What you’ll hear is questionable at best
  3. Make your code easy to read for other developers.

Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images