001/*
002 * Trident - A Multithreaded Server Alternative
003 * Copyright 2014 The TridentSDK Team
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package net.tridentsdk.server.concurrent;
019
020import net.tridentsdk.config.Config;
021import net.tridentsdk.registry.Registered;
022import net.tridentsdk.server.util.ConcurrentCircularArray;
023import net.tridentsdk.server.world.TridentWorld;
024import net.tridentsdk.server.world.TridentWorldLoader;
025import net.tridentsdk.util.TridentLogger;
026import net.tridentsdk.world.World;
027
028import javax.annotation.concurrent.ThreadSafe;
029import java.nio.file.Paths;
030import java.util.Iterator;
031import java.util.concurrent.TimeUnit;
032import java.util.concurrent.atomic.AtomicInteger;
033
034/**
035 * Handles the running of the server, the "ticks" that occur 20 times a second
036 *
037 * @author The TridentSDK Team
038 */
039@ThreadSafe
040public class MainThread extends Thread {
041    private static final boolean FINISH_TASKS_LEFT = new Config(Paths.get("server.json"))
042            .getConfigSection("performance")
043            .getBoolean("finish-tasks-left");
044    private static final int RECENT_TICKS_KEPT = 40;
045    private static final String NAME = "Trident - Tick Thread";
046
047    private final AtomicInteger ticksElapsed = new AtomicInteger();
048    private final AtomicInteger notLostTicksElapsed = new AtomicInteger();
049    private final AtomicInteger ticksToWait = new AtomicInteger();
050    private final int ticksPerSecond;
051    private final int tickLength;
052
053    private final ConcurrentCircularArray<Integer> recentTickLength = new ConcurrentCircularArray<>(RECENT_TICKS_KEPT);
054
055    /**
056     * system.currenttimemillis() when the server's first tick happened, used to keep on schedule, subject to change
057     * when the server is running slow
058     */
059    private long zeroBase;
060    private volatile boolean pausedTicking;
061    private volatile boolean redstoneTick;
062
063    /**
064     * Creates the MainThread runner from the amount of heartbeats the server should take per second the server runs
065     *
066     * @param ticksPerSecond the amount of heartbeats per second
067     */
068    public MainThread(int ticksPerSecond) {
069        super(NAME);
070        this.zeroBase = System.currentTimeMillis();
071        this.ticksPerSecond = ticksPerSecond;
072        this.tickLength = 1000 / ticksPerSecond;
073    }
074
075    public void doRun() throws InterruptedException {
076        long startTime = System.currentTimeMillis();
077
078        this.ticksElapsed.getAndIncrement();
079
080        // if we've paused, wait and then skip the rest
081        if (this.pausedTicking) {
082            this.calcAndWait(0);
083            return;
084        }
085
086        if (this.ticksToWait.get() > 0) {
087            this.ticksToWait.getAndDecrement();
088            this.calcAndWait(0);
089            return;
090        }
091
092        this.notLostTicksElapsed.getAndIncrement();
093
094        // Entities are ticked by the world
095        for (World world : TridentWorldLoader.WORLDS.values()) {
096            TickSync.increment("WORLD: " + world.name());
097            ((TridentWorld) world).tick();
098        }
099
100        // TODO: check the worlds to make sure they're not suffering
101
102        ((TridentTaskScheduler) Registered.tasks()).tick();
103
104        TickSync.awaitSync();
105
106        long time;
107        while ((time = System.currentTimeMillis() - startTime) < tickLength) {
108            Runnable next = TickSync.waitForTask(TimeUnit.NANOSECONDS.convert(time, TimeUnit.NANOSECONDS));
109            if (next != null) {
110                Registered.plugins().executor().execute(next);
111            }
112        }
113
114        if (!FINISH_TASKS_LEFT) {
115            int left = TickSync.left();
116            if (left > 0) {
117                TridentLogger.get().warn("Skipped " + left + " plugin task(s) this tick");
118            }
119        } else {
120            while (TickSync.left() > 0) {
121                Runnable runnable = TickSync.next();
122                if (runnable != null) runnable.run();
123            }
124        }
125
126        TickSync.reset();
127        recentTickLength.add((int) (System.currentTimeMillis() - startTime));
128    }
129
130    @Override
131    public void run() {
132        super.run();
133
134        while (!this.isInterrupted()) {
135            try {
136                doRun();
137            } catch (InterruptedException e) {
138                break;
139            }
140        }
141    }
142
143    /**
144     * Interesting new feature (relative to other implementations) that would allow it to pause ticking
145     */
146    public boolean pauseTicking() {
147        // TODO: configurable
148        this.pausedTicking = true;
149        return true;
150    }
151
152    /**
153     * Calculates how long it should wait, and waits for that amount of time
154     *
155     * @param tit the Time in Tick (tit)
156     */
157    private void calcAndWait(int tit) {
158        this.correctTiming();
159
160        int ttw = this.tickLength - tit;
161
162        if (ttw <= 0) {
163            return;
164        }
165
166        try {
167            Thread.sleep((long) ttw);
168        } catch (InterruptedException ex) {
169            this.interrupt();
170        }
171    }
172
173    // TODO HOT CONCURRENT METHOD NOT INLINEABLE: TOO LARGE
174    // 54 bytes
175    private void correctTiming() {
176        long expectedTime = (long) ((this.ticksElapsed.get() - 1) * this.tickLength);
177        long actualTime = System.currentTimeMillis() - this.zeroBase;
178        if (actualTime != expectedTime) {
179            // if there is a difference of less than two milliseconds, just update zerobase to compensate and maintain
180            // accuracy
181            if (actualTime - expectedTime <= 2L) {
182                this.zeroBase += actualTime - expectedTime;
183            }
184
185            // handle more advanced divergences
186        }
187    }
188
189    /**
190     * Instead of needing to be resumed, it will instead just skip this many ticks and resume
191     *
192     * @param ticks the ticks to pause for
193     * @return whether the server allows the pausing of ticking
194     * @see MainThread#pauseTicking()
195     */
196    public boolean pauseTicking(int ticks) {
197        this.ticksToWait.addAndGet(ticks);
198        return true;
199    }
200
201    /**
202     * Complement to MainThread#pauseTicking(), resumes the ticking of the server
203     */
204    public void resumeTicking() {
205        this.pausedTicking = false;
206    }
207
208    @Override
209    public void interrupt() {
210        super.interrupt();
211    }
212
213    @Override
214    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
215        return super.getUncaughtExceptionHandler();
216    }
217
218    /**
219     * Used to get the length of the average tick for the past 40 ticks
220     *
221     * <p>Does not count ticks that have been skipped via MainThread#pauseTicking()</p>
222     *
223     * @return the average length of a tick for the past 40 ticks
224     */
225    public double getAverageTickLength() {
226        Iterator<Integer> iter = recentTickLength.iterator();
227
228        double total = 0d;
229        while (iter.hasNext()) {
230            total += iter.next();
231        }
232
233        return total / RECENT_TICKS_KEPT;
234    }
235
236    /**
237     * Gets the elapsed ticks
238     *
239     * @return the ticks elapsed
240     */
241    public int getTicksElapsed() {
242        return this.ticksElapsed.get();
243    }
244
245    /**
246     * Gets the ticks that were not lost in the time elapsed
247     *
248     * @return the ticks not lost
249     */
250    public int getNotLostTicksElapsed() {
251        return this.notLostTicksElapsed.get();
252    }
253}