/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.runtime.extensions.timer;
import static com.github.anba.es6draft.runtime.AbstractOperations.IsCallable;
import static com.github.anba.es6draft.runtime.AbstractOperations.ToFlatString;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.github.anba.es6draft.Script;
import com.github.anba.es6draft.compiler.CompilationException;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Task;
import com.github.anba.es6draft.runtime.internal.Properties.Function;
import com.github.anba.es6draft.runtime.internal.Source;
import com.github.anba.es6draft.runtime.internal.TaskSource;
import com.github.anba.es6draft.runtime.types.Callable;
/**
* Simple <code>Timers</code> implementation.
*
* @see <a
* href="http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#timers">Web
* application APIs - Timers</a>
*/
public final class Timers implements TaskSource {
private static final int MAX_TIMEOUT_NESTING = 5;
private static final int TIMER_CLAMP_TIMEOUT = 0;
private static final int TIMER_CLAMP_INTERVAL = 4;
private static final int MAX_TIMEOUT = Integer.MAX_VALUE;
private final AtomicInteger timerIds = new AtomicInteger();
private final DelayQueue<TimerTask> queue = new DelayQueue<>();
private final ConcurrentHashMap<Integer, TimerTask> activeTimers = new ConcurrentHashMap<>(16, 0.75f, 2);
private int nestingLevel = 0;
private abstract class TimerTask implements Task, Delayed {
private final int timerId;
private final long delay;
private final boolean interval;
private boolean cancelled = false;
private long time;
protected TimerTask(long delay, boolean interval) {
this.timerId = timerIds.incrementAndGet();
this.delay = delay;
this.interval = interval;
this.time = nextStart();
}
private long nextStart() {
return System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delay);
}
int getTimerId() {
return timerId;
}
boolean isInterval() {
return interval;
}
void cancel() {
if (!cancelled) {
cancelled = true;
queue.remove(this);
}
}
@Override
public int compareTo(Delayed o) {
if (o == this) {
return 0;
}
if (o instanceof TimerTask) {
TimerTask x = (TimerTask) o;
long delta = time - x.time;
return delta < 0 ? -1 : delta > 0 ? 1 : timerId < x.timerId ? -1 : 1;
}
long delta = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return delta == 0 ? 0 : delta < 0 ? -1 : 1;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public final void execute() {
if (!cancelled) {
long nextStart = interval ? nextStart() : 0;
try {
nestingLevel++;
executeInner();
} finally {
nestingLevel--;
if (!cancelled && interval) {
time = nextStart;
queue.offer(this);
}
}
}
}
protected abstract void executeInner();
}
private final class CallableTimerTask extends TimerTask {
private final ExecutionContext cx;
private final Callable f;
private final Object[] args;
CallableTimerTask(long delay, boolean interval, ExecutionContext cx, Callable f, Object... args) {
super(delay, interval);
this.cx = cx;
this.f = f;
this.args = args;
}
@Override
protected void executeInner() {
f.call(cx, cx.getRealm().getGlobalThis(), args);
}
}
private final class ScriptedTimerTask extends TimerTask {
private final ExecutionContext cx;
private final String sourceCode;
ScriptedTimerTask(long delay, boolean interval, ExecutionContext cx, String sourceCode) {
super(delay, interval);
this.cx = cx;
this.sourceCode = sourceCode;
}
@Override
protected void executeInner() {
Source source = new Source(cx.getRealm().sourceInfo(cx), "<Timer>", 1);
Script script;
try {
script = cx.getRealm().getScriptLoader().script(source, sourceCode);
} catch (ParserException | CompilationException e) {
throw e.toScriptException(cx);
}
script.evaluate(cx);
}
}
private TimerTask scheduleTimer(long delay, boolean interval, ExecutionContext cx, Object f, Object... args) {
TimerTask task;
if (IsCallable(f)) {
task = new CallableTimerTask(delay, interval, cx, (Callable) f, args);
} else {
task = new ScriptedTimerTask(delay, interval, cx, ToFlatString(cx, f));
}
activeTimers.put(task.getTimerId(), task);
queue.offer(task);
return task;
}
private void cancelTimer(int timerId) {
TimerTask task = activeTimers.remove(timerId);
if (task != null) {
task.cancel();
}
}
@Override
public Task nextTask() throws InterruptedException {
if (queue.isEmpty()) {
return null;
}
return awaitTask();
}
@Override
public Task awaitTask() throws InterruptedException {
TimerTask task = queue.take();
if (!task.isInterval()) {
activeTimers.remove(task.getTimerId());
}
return task;
}
@Function(name = "setTimeout", arity = 2)
public int setTimeout(ExecutionContext cx, Object f, double timeout, Object... args) {
int delay = (int) Math.min(Math.max(timeout, TIMER_CLAMP_TIMEOUT), MAX_TIMEOUT);
if (nestingLevel > MAX_TIMEOUT_NESTING) {
delay = Math.max(delay, TIMER_CLAMP_INTERVAL);
}
TimerTask task = scheduleTimer(delay, false, cx, f, args);
return task.getTimerId();
}
@Function(name = "setInterval", arity = 2)
public int setInterval(ExecutionContext cx, Object f, double timeout, Object... args) {
int delay = (int) Math.min(Math.max(timeout, TIMER_CLAMP_INTERVAL), MAX_TIMEOUT);
TimerTask task = scheduleTimer(delay, true, cx, f, args);
return task.getTimerId();
}
@Function(name = "clearTimeout", arity = 1)
public void clearTimeout(ExecutionContext cx, int handle) {
cancelTimer(handle);
}
@Function(name = "clearInterval", arity = 1)
public void clearInterval(ExecutionContext cx, int handle) {
cancelTimer(handle);
}
}