Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Issue: setTimeout/setInterval #244

Open
theonlypwner opened this issue Oct 12, 2015 · 7 comments
Open

Performance Issue: setTimeout/setInterval #244

theonlypwner opened this issue Oct 12, 2015 · 7 comments

Comments

@theonlypwner
Copy link

It makes more sense to use setTimeout instead of setInterval.

If this is done, the function can determine when the text is actually going to change and not waste calls that don't do anything. If the text says 2 years ago, it's probably not going to change in a minute, so it makes more sense to calculate the time delay.

Also, this would be more efficient for a modified version that shows %d seconds ago and needs to update at 1000 ms intervals only before it becomes a minute. Then it can change the interval to 60000 ms.

@edwh
Copy link

edwh commented Oct 1, 2016

I see this as a genuine performance issue on a page which uses timeago on many elements.

@edwh
Copy link

edwh commented Oct 1, 2016

Here's a version (based on a slightly older base) which does this, and also doesn't keep the timer running if the element is no longer in the DOM.

/*!
 * Timeago is a jQuery plugin that makes it easy to support automatically
 * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
 *
 * @name timeago
 * @version 1.4.0
 * @requires jQuery v1.2.3+
 * @author Ryan McGeary
 * @license MIT License - http://www.opensource.org/licenses/mit-license.php
 *
 * For usage and examples, visit:
 * http://timeago.yarp.com/
 *
 * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
 * 
 * Modified by EH
 */

var timeAgoId = 1;

(function(factory){
    if(typeof define === 'function' && define.amd){
        // AMD. Register as an anonymous module.
        define(['jquery'], factory);
    }else{
        // Browser globals
        factory(jQuery);
    }
}(function($){
    $.timeago = function(timestamp){
        if(timestamp instanceof Date){
            return inWords(timestamp);
        }else if(typeof timestamp === "string"){
            return inWords($.timeago.parse(timestamp));
        }else if(typeof timestamp === "number"){
            return inWords(new Date(timestamp));
        }else{
            return inWords($.timeago.datetime(timestamp));
        }
    };
    var $t = $.timeago;

    $.extend($.timeago, {
        settings: {
            refreshMillis: 60000,
            allowPast    : true,
            allowFuture  : false,
            localeTitle  : false,
            cutoff       : 0,
            strings      : {
                prefixAgo    : null,
                prefixFromNow: null,
                suffixAgo    : "ago",
                suffixFromNow: "from now",
                inPast       : 'any moment now',
                seconds      : "less than a minute",
                minute       : "about a minute",
                minutes      : "%d minutes",
                hour         : "about an hour",
                hours        : "about %d hours",
                day          : "a day",
                days         : "%d days",
                month        : "about a month",
                months       : "%d months",
                year         : "about a year",
                years        : "%d years",
                wordSeparator: " ",
                numbers      : []
            }
        },

        inWords: function(distanceMillis){
            if(!this.settings.allowPast && !this.settings.allowFuture){
                throw 'timeago allowPast and allowFuture settings can not both be set to false.';
            }

            var $l = this.settings.strings;
            var prefix = $l.prefixAgo;
            var suffix = $l.suffixAgo;
            if(this.settings.allowFuture){
                if(distanceMillis < 0){
                    prefix = $l.prefixFromNow;
                    suffix = $l.suffixFromNow;
                }
            }

            if(!this.settings.allowPast && distanceMillis >= 0){
                return this.settings.strings.inPast;
            }

            var seconds = Math.abs(distanceMillis) / 1000;
            var minutes = seconds / 60;
            var hours = minutes / 60;
            var days = hours / 24;
            var years = days / 365;

            function substitute(stringOrFunction, number){
                var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
                var value = ($l.numbers && $l.numbers[number]) || number;
                return string.replace(/%d/i, value);
            }

            var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
                seconds < 90 && substitute($l.minute, 1) ||
                minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
                minutes < 90 && substitute($l.hour, 1) ||
                hours < 24 && substitute($l.hours, Math.round(hours)) ||
                hours < 42 && substitute($l.day, 1) ||
                days < 30 && substitute($l.days, Math.round(days)) ||
                days < 45 && substitute($l.month, 1) ||
                days < 365 && substitute($l.months, Math.round(days / 30)) ||
                years < 1.5 && substitute($l.year, 1) ||
                substitute($l.years, Math.round(years));

            var separator = $l.wordSeparator || "";
            if($l.wordSeparator === undefined){
                separator = " ";
            }
            return $.trim([prefix, words, suffix].join(separator));
        },

        parse   : function(iso8601){
            var s = $.trim(iso8601);
            s = s.replace(/\.\d+/, ""); // remove milliseconds
            s = s.replace(/-/, "/").replace(/-/, "/");
            s = s.replace(/T/, " ").replace(/Z/, " UTC");
            s = s.replace(/([\+\-]\d\d)\:?(\d\d)/, " $1$2"); // -04:00 -> -0400
            s = s.replace(/([\+\-]\d\d)$/, " $100"); // +09 -> +0900
            return new Date(s);
        },
        datetime: function(elem){
            var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
            return $t.parse(iso8601);
        },
        isTime  : function(elem){
            // jQuery's `is()` doesn't play well with HTML5 in IE
            return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
        }
    });

    // functions that can be called via $(el).timeago('action')
    // init is default when no action is given
    // functions are called with context of a single element
    var functions = {
        init         : function(){
            this.id = timeAgoId++;
            var refresh_el = $.proxy(refresh, this);
            refresh_el(true);
        },
        update       : function(time){
            var parsedTime = $t.parse(time);
            $(this).data('timeago', { datetime: parsedTime });
            if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
            refresh.apply(this);
        },
        updateFromDOM: function(){
            $(this).data('timeago', { datetime: $t.parse($t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title")) });
            refresh.apply(this);
        },
        dispose      : function(){
            if(this._timeagoInterval){
                window.clearInterval(this._timeagoInterval);
                this._timeagoInterval = null;
            }
        }
    };

    $.fn.timeago = function(action, options){
        var fn = action ? functions[action] : functions.init;
        if(!fn){
            throw new Error("Unknown function name '" + action + "' for timeago");
        }
        // each over objects here and call the requested function
        this.each(function(){
            fn.call(this, options);
        });
        return this;
    };

    function refresh(first){
        if (first || $(this).closest('body').length) {
            var data = prepareData(this);
            var $s = $t.settings;
            var nextTime = $s.refreshMillis;

            if(!isNaN(data.datetime)){
                var dist = distance(data.datetime);

                if($s.cutoff == 0 || dist < $s.cutoff){
                    $(this).text(inWords(data.datetime));
                }

                var seconds = Math.abs(dist) / 1000;
                var minutes = seconds / 60;
                var hours = minutes / 60;
                var days = hours / 24;
                var years = days / 365;

                // If the next change is a long time away, set the timer appropriately.
                if (years > 1) {
                    nextTime = 365 * 24 * 60 * 60 * 1000 / 2;
                } else if (days > 1) {
                    nextTime = 24 * 60 * 60 * 1000 / 2;
                } else if (hours > 1) {
                    nextTime = 60 * 60 * 1000 / 2;
                } else if (minutes > 1) {
                    nextTime = 60 * 1000 / 2;
                } 
            }

            var refresh_el = $.proxy(refresh, this);
            var $s = $t.settings;
            if($s.refreshMillis > 0){
                this._timeagoInterval = setTimeout(refresh_el, nextTime);
            }
        }

        return this;
    }

    function prepareData(element){
        element = $(element);
        if(!element.data("timeago")){
            element.data("timeago", { datetime: $t.datetime(element) });
            var text = $.trim(element.text());
            if($t.settings.localeTitle){
                element.attr("title", element.data('timeago').datetime.toLocaleString());
            }else if(text.length > 0 && !($t.isTime(element) && element.attr("title"))){
                element.attr("title", text);
            }
        }
        return element.data("timeago");
    }

    function inWords(date){
        return $t.inWords(distance(date));
    }

    function distance(date){
        return (new Date().getTime() - date.getTime());
    }

    // fix for IE6 suckage
    document.createElement("abbr");
    document.createElement("time");
}));

@theonlypwner
Copy link
Author

theonlypwner commented Oct 1, 2016

@edwh, I only took a quick look at your code, but I think it has a bug.

a year ago (1.4 years ago) becomes 2 years ago after 0.1 years (that's the current behavior because of rounding). However, it won't update until it becomes 2.4 years ago.

@theonlypwner
Copy link
Author

Also, it might not work for future dates.

a year from now updates after 1 month, not 1 year, to 11 months from now.

@edwh
Copy link

edwh commented Oct 1, 2016

@theonlypwner Even with the / 2? I would expect that to cause it to check again after a further 0.5 years. But even then it should really update sooner than that if it was to preserve the switchover at 1.5 years, so you're right it's bugged.

For my immediate purposes that's ok - I'm only interested in something rough for past dates only, so if it's a minute or two out I don't much mind. The performance hit of having a zillion timers running was a killer - particularly using setInterval, because once you had more than a certain number of timers running it wouldn't be able to keep up and so would flatline in timer processing. I did wonder about trying to avoid having a per-usage timer at all, but, y'know, other fish to fry.

I may come back to this and do a less hasty fix, but don't hold your breath :-)

@theonlypwner
Copy link
Author

All %d years dates are updated at the same time.
It's probably better to group timers for yearly, monthly, daily, etc. updates.
After a timer is run, it could possibly become an interval.

@theonlypwner
Copy link
Author

theonlypwner commented Aug 17, 2017

Unless you can tolerate error of up to 1 year, disregard what I said about grouping timers, as 2 years ago for one timer could be supposed to update a few second after another timer that says the same. Updating all of the "year" timers at the same time could be more efficient (less timers), but less accurate.


The timeago.org plugin updates the timer 1 second after immediately when it's supposed to change. It makes sense to set the timer like this (but maybe 1 ms after the time instead of 1 entire second).

  /**
   * nextInterval: calculate the next interval time.
   * - diff: the diff sec between now and date to be formated.
   *
   * What's the meaning?
   * diff = 61 then return 59
   * diff = 3601 (an hour + 1 second), then return 3599
   * make the interval with high performace.
  **/
  function nextInterval(diff) {
    var rst = 1, i = 0, d = Math.abs(diff);
    for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
      diff /= SEC_ARRAY[i];
      rst *= SEC_ARRAY[i];
    }
    // return leftSec(d, rst);
    d = d % rst;
    d = d ? rst - d : rst;
    return Math.ceil(d);
  }

https://github.com/hustcc/timeago.js/blob/310864d574c89cb692c0395dd7fe650cfa12fba7/src/timeago.js#L80-L99

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants