Android Java - Joda Date is slow
Asked Answered
S

11

57

Using Joda 1.6.2 with Android

The following code hangs for about 15 seconds.

DateTime dt = new DateTime();

Originally posted this post Android Java - Joda Date is slow in Eclipse/Emulator -

Just tried it again and its still not any better. Does anyone else have this problem or know how to fix it?

Sharpsighted answered 20/2, 2011 at 20:3 Comment(0)
J
64

I also ran into this problem. Jon Skeet's suspicions were correct, the problem is that the time zones are being loaded really inefficiently, opening a jar file and then reading the manifest to try to get this information.

However, simply calling DateTimeZone.setProvider([custom provider instance ...]) is not sufficient because, for reasons that don't make sense to me, DateTimeZone has a static initializer where it calls getDefaultProvider().

To be completely safe, you can override this default by setting this system property before you ever call anything in the joda.

In your activity, for example, add this:

@Override
public void onCreate(Bundle savedInstanceState) {
    System.setProperty("org.joda.time.DateTimeZone.Provider", 
    "com.your.package.FastDateTimeZoneProvider");
}

Then all you have to do is define FastDateTimeZoneProvider. I wrote the following:

package com.your.package;

public class FastDateTimeZoneProvider implements Provider {
    public static final Set<String> AVAILABLE_IDS = new HashSet<String>();

    static {
        AVAILABLE_IDS.addAll(Arrays.asList(TimeZone.getAvailableIDs()));
    }

    public DateTimeZone getZone(String id) {
        if (id == null) {
            return DateTimeZone.UTC;
        }

        TimeZone tz = TimeZone.getTimeZone(id);
        if (tz == null) {
            return DateTimeZone.UTC;
        }

        int rawOffset = tz.getRawOffset();

            //sub-optimal. could be improved to only create a new Date every few minutes
        if (tz.inDaylightTime(new Date())) {
            rawOffset += tz.getDSTSavings();
        }

        return DateTimeZone.forOffsetMillis(rawOffset);
    }

    public Set getAvailableIDs() {
        return AVAILABLE_IDS;
    }
}

I've tested this and it appears to work on Android SDK 2.1+ with joda version 1.6.2. It can of course be optimized further, but while profiling my app (mogwee), this decreased the DateTimeZone initialize time from ~500ms to ~18ms.

If you are using proguard to build your app, you'll have to add this line to proguard.cfg because Joda expects the class name to be exactly as you specify:

-keep class com.your.package.FastDateTimeZoneProvider
Jellify answered 9/6, 2011 at 19:35 Comment(8)
Thanks for code. I was working on some automation, and it turned out that on emulator with API Level 11 ActivityManager was crashing my process under test specifically because of timeout on JodaTime initialization. What's weird though, is that all other emulators (8-15 levels) were working just fine. Probably 11th is the slowest one :)Ondrea
@Jellify great job. this also solved the issue of me not getting the correct local time at all when using joda in android.Ultann
You said "sub-optimal". Is replacing new Date() with getNow() sufficient?Clearwing
+1, excellent. It would be awesome if you could upload a version to a gist with a license other than SO's CC BY-SA. Here's my version that's Apache licensed: gist.github.com/orip/6686755Stockjobber
I think your code has a serious flaw if I understood it right. Suppose your DateTime object is set for 4:00am, one hour after daylight savings have started. When Joda checks this provider before 3:00am (i.e., before daylight savings) it will get a DateTimeZone with the wrong offset because your tz.inDaylightTime(new Date()) will return false. I still don't know how to best fix this though.Tamtam
I wrote an answer below with an alternative solution.Tamtam
Though this is a fantastic solution, beware that it returns timezones that are DST-naive (though the choice of timezone returned is DST-correct at the moment of computation). An alternative would be to return a custom implementation of DateTimeZone that delegates to the returned java.util.TimeZone, acknowledging the performance hazards posed in this Joda newsgroup thread.Faller
Any update wrt. Java8 time for newer Android API versions?Align
D
23

I strongly suspect it's because it's having to build the ISO chronology for the default time zone, which probably involves reading all the time zone information in.

You could verify this by calling ISOChronology.getInstance() first - time that, and then time a subsequent call to new DateTime(). I suspect it'll be fast.

Do you know which time zones are going to be relevant in your application? You may find you can make the whole thing much quicker by rebuilding Joda Time with a very much reduced time zone database. Alternatively, call DateTimeZone.setProvider() with your own implementation of Provider which doesn't do as much work.

It's worth checking whether that's actually the problem first, of course :) You may also want to try explicitly passing in the UTC time zone, which won't require reading in the time zone database... although you never know when you'll accidentally trigger a call which does require the default time zone, at which point you'll incur the same cost.

Drug answered 20/2, 2011 at 20:39 Comment(2)
Calling the ISOChronology took time as expected. The subsequent call to DateTime() was instant. I am not sure how you pass the time zone so I didn't try that. Also sorry for the delay in response, I had major surgery and just getting back.Sharpsighted
I took the rebuilding with a smaller TZ database option. I blanked out the Olson TZDB sources leaving just the rules for the four or five TZs my app needs. This seems to fix the performance problem quite well.Provocative
M
10

I only need UTC in my application. So, following unchek's advice, I used

System.setProperty("org.joda.time.DateTimeZone.Provider", "org.joda.time.tz.UTCProvider");

org.joda.time.tz.UTCProvider is actually used by JodaTime as the secondary backup, so I thought why not use it for primary use? So far so good. It loads fast.

Marable answered 7/7, 2011 at 3:52 Comment(0)
T
8

The top answer provided by plowman is not reliable if you must have precise timezone computations for your dates. Here is an example of problem that can happen:

Suppose your DateTime object is set for 4:00am, one hour after daylight savings have started that day. When Joda checks the FastDateTimeZoneProvider provider before 3:00am (i.e., before daylight savings) it will get a DateTimeZone object with the wrong offset because the tz.inDaylightTime(new Date()) check will return false.

My solution was to adopt the recently published joda-time-android library. It uses the core of Joda but makes sure to load a time zone only as needed from the raw folder. Setting up is easy with gradle. In your project, extend the Application class and add the following on its onCreate():

public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        JodaTimeAndroid.init(this);
    }
}

The author wrote a blog post about it last year.

Tamtam answered 9/3, 2014 at 13:33 Comment(1)
in the latest version, init static method has been moved to JodaTimeAndroid class.Thurstan
H
3

I can confirm this issue with version 1, 1.5 and 1.62 of joda. Date4J is working well for me as an alternative.

http://www.date4j.net/

Higgler answered 10/3, 2011 at 1:24 Comment(0)
V
3

I just performed the test that @"Name is carl" posted, on several devices. I must note that the test is not completely valid and the results are misleading (in that it only reflects a single instance of DateTime).

  1. From his test, When comparing DateTime to Date, DateTime is forced to parse the String ts, where Date does not parse anything.

  2. While the initial creation of the DateTime was accurate, it ONLY takes that much time on the very FIRST creation... every instance after that was 0ms (or very near 0ms)

To verify this, I used the following code and created 1000 new instances of DateTime on an OLD Android 2.3 device

    int iterations = 1000;
    long totalTime = 0;

    // Test Joda Date
    for (int i = 0; i < iterations; i++) {
        long d1 = System.currentTimeMillis();
        DateTime d = new DateTime();
        long d2 = System.currentTimeMillis();

        long duration = (d2 - d1);
        totalTime += duration;
        log.i(TAG, "datetime : " + duration);
    }
    log.i(TAG, "Average datetime : " + ((double) totalTime/ (double) iterations));

My results showed:

datetime : 264
datetime : 0
datetime : 0
datetime : 0
datetime : 0
datetime : 0
datetime : 0
...
datetime : 0
datetime : 0
datetime : 1
datetime : 0
...
datetime : 0
datetime : 0
datetime : 0

So, the result was that the first instance was 264ms and more than 95% of the following were 0ms (I occasionally had a 1ms, but never had a value larger than 1ms).

Hope this gives a clearer picture of the cost of using Joda.

NOTE: I was using joda-time version 2.1

Vicechancellor answered 22/2, 2013 at 22:50 Comment(0)
D
3

Using dlew/joda-time-android gradle dependency it takes only 22.82 ms (milliseconds). So I recommend you using it instead of overriding anything.

Divisive answered 27/3, 2015 at 20:48 Comment(0)
T
2

I found solution for me. I load UTC and default time zone. So it's loads very fast. And I think in this case I need catch broadcast TIME ZONE CHANGE and reload default time zone.

public class FastDateTimeZoneProvider implements Provider {
    public static final Set<String> AVAILABLE_IDS = new HashSet<String>();
    static {
        AVAILABLE_IDS.add("UTC");
        AVAILABLE_IDS.add(TimeZone.getDefault().getID());
    }

    public DateTimeZone getZone(String id) {
        int rawOffset = 0;

        if (id == null) {
            return DateTimeZone.getDefault();
        }

        TimeZone tz = TimeZone.getTimeZone(id);
        if (tz == null) {
            return DateTimeZone.getDefault();
        }

        rawOffset = tz.getRawOffset();

        //sub-optimal. could be improved to only create a new Date every few minutes
        if (tz.inDaylightTime(new Date())) {
            rawOffset += tz.getDSTSavings();
        }
        return DateTimeZone.forOffsetMillis(rawOffset);
    }

    public Set getAvailableIDs() {
        return AVAILABLE_IDS;
    }
}
Tricia answered 26/11, 2012 at 18:33 Comment(1)
And here is code for update default time zone DateTimeZone dateTimeZone = DateTimeZone.forTimeZone(TimeZone.getDefault()); DateTimeZone.setDefault(dateTimeZone);Tricia
A
1

This quick note to complete the answer about date4j from @Steven

I ran a quick and dirty benchmark comparing java.util.Date, jodatime and date4j on the weakest android device I have (HTC Dream/Sapphire 2.3.5).

Details : normal build (no proguard), implementing the FastDateTimeZoneProvider for jodatime.

Here's the code:

String ts = "2010-01-19T23:59:59.123456789";
long d1 = System.currentTimeMillis();
DateTime d = new DateTime(ts);
long d2 = System.currentTimeMillis();
System.err.println("datetime : " + dateUtils.durationtoString(d2 - d1));
d1 = System.currentTimeMillis();
Date dd = new Date();
d2 = System.currentTimeMillis();
System.err.println("date : " + dateUtils.durationtoString(d2 - d1));

d1 = System.currentTimeMillis();
hirondelle.date4j.DateTime ddd = new hirondelle.date4j.DateTime(ts);
d2 = System.currentTimeMillis();
System.err.println("date4j : " + dateUtils.durationtoString(d2 - d1));

Here are the results :

           debug       | normal 
joda     : 3s (3577ms) | 0s (284ms)
date     : 0s (0)      | 0s (0s)
date4j   : 0s (55ms)   | 0s (2ms) 

One last thing, the jar sizes :

jodatime 2.1 : 558 kb 
date4j       : 35 kb

I think I'll give date4j a try.

Arlberg answered 11/12, 2012 at 18:2 Comment(0)
H
1

You could also checkout Jake Wharton's JSR-310 backport of the java.time.* packages.

This library places the timezone information as a standard Android asset and provides a custom loader for parsing it efficiently. [It] offers the standard APIs in Java 8 as a much smaller package in not only binary size and method count, but also in API size.

Thus, this solution provides a smaller binary-size library with a smaller method count footprint, combined with an efficient loader for Timezone data.

Holinshed answered 14/1, 2018 at 7:55 Comment(0)
D
0
  • As already mentioned you could use the joda-time-android library.
  • Do not use FastDateTimeZoneProvider proposed by @ElijahSh and @plowman. Because it is treat DST offset as standart offset for the selected timezone. As it will give "right" results for the today and for the rest of a half of a year before the next DST transition occurs. But it will defenetly give wrong result for the day before DST transition, and for the day after next DST transition.
  • The right way to utilize system's timezones with JodaTime:

    public class AndroidDateTimeZoneProvider implements org.joda.time.tz.Provider {

    @Override
    public Set<String> getAvailableIDs() {
        return new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs()));
    }    
    
    @Override
    public DateTimeZone getZone(String id) {
        return id == null
                ? null
                : id.equals("UTC")
                    ? DateTimeZone.UTC
                    : Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                        ? new AndroidNewDateTimeZone(id)
                        : new AndroidOldDateTimeZone(id);
    }
    

    }

Where AndroidOldDateTimeZone:

public class AndroidOldDateTimeZone extends DateTimeZone {

    private final TimeZone mTz;
    private final Calendar mCalendar;
    private long[] mTransition;

    public AndroidOldDateTimeZone(final String id) {
        super(id);
        mTz = TimeZone.getTimeZone(id);
        mCalendar = GregorianCalendar.getInstance(mTz);
        mTransition = new long[0];

        try {
            final Class tzClass = mTz.getClass();
            final Field field = tzClass.getDeclaredField("mTransitions");
            field.setAccessible(true);
            final Object transitions = field.get(mTz);

            if (transitions instanceof long[]) {
                mTransition = (long[]) transitions;
            } else if (transitions instanceof int[]) {
                final int[] intArray = (int[]) transitions;
                final int size = intArray.length;
                mTransition = new long[size];
                for (int i = 0; i < size; i++) {
                    mTransition[i] = intArray[i];
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public TimeZone getTz() {
        return mTz;
    }

    @Override
    public long previousTransition(final long instant) {
        if (mTransition.length == 0) {
            return instant;
        }

        final int index = findTransitionIndex(instant, false);

        if (index <= 0) {
            return instant;
        }

        return mTransition[index - 1] * 1000;
    }

    @Override
    public long nextTransition(final long instant) {
        if (mTransition.length == 0) {
            return instant;
        }

        final int index = findTransitionIndex(instant, true);

        if (index > mTransition.length - 2) {
            return instant;
        }

        return mTransition[index + 1] * 1000;
    }

    @Override
    public boolean isFixed() {
        return mTransition.length > 0 &&
               mCalendar.getMinimum(Calendar.DST_OFFSET) == mCalendar.getMaximum(Calendar.DST_OFFSET) &&
               mCalendar.getMinimum(Calendar.ZONE_OFFSET) == mCalendar.getMaximum(Calendar.ZONE_OFFSET);
    }

    @Override
    public boolean isStandardOffset(final long instant) {
        mCalendar.setTimeInMillis(instant);
        return mCalendar.get(Calendar.DST_OFFSET) == 0;
    }

    @Override
    public int getStandardOffset(final long instant) {
        mCalendar.setTimeInMillis(instant);
        return mCalendar.get(Calendar.ZONE_OFFSET);
    }

    @Override
    public int getOffset(final long instant) {
        return mTz.getOffset(instant);
    }

    @Override
    public String getShortName(final long instant, final Locale locale) {
        return getName(instant, locale, true);
    }

    @Override
    public String getName(final long instant, final Locale locale) {
        return getName(instant, locale, false);
    }

    private String getName(final long instant, final Locale locale, final boolean isShort) {
        return mTz.getDisplayName(!isStandardOffset(instant),
               isShort ? TimeZone.SHORT : TimeZone.LONG,
               locale == null ? Locale.getDefault() : locale);
    }

    @Override
    public String getNameKey(final long instant) {
        return null;
    }

    @Override
    public TimeZone toTimeZone() {
        return (TimeZone) mTz.clone();
    }

    @Override
    public String toString() {
        return mTz.getClass().getSimpleName();
    }

    @Override
    public boolean equals(final Object o) {
        return (o instanceof AndroidOldDateTimeZone) && mTz == ((AndroidOldDateTimeZone) o).getTz();
    }

    @Override
    public int hashCode() {
        return 31 * super.hashCode() + mTz.hashCode();
    }

    private long roundDownMillisToSeconds(final long millis) {
        return millis < 0 ? (millis - 999) / 1000 : millis / 1000;
    }

    private int findTransitionIndex(final long millis, final boolean isNext) {
        final long seconds = roundDownMillisToSeconds(millis);
        int index = isNext ? mTransition.length : -1;
        for (int i = 0; i < mTransition.length; i++) {
            if (mTransition[i] == seconds) {
                index = i;
            }
        }
        return index;
    }
}

The AndroidNewDateTimeZone.java same as "Old" one but based on android.icu.util.TimeZone instead.

  • I have created a fork of Joda Time especially for this. It loads for only ~29 ms in debug mode and ~2ms in release mode. Also it has less weight as it doesn't include timezone database.
Diphenylhydantoin answered 21/5, 2019 at 11:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.