├── .editorconfig ├── .gitattributes ├── .gitignore ├── IMPLEMENTATION.md ├── INSTALL.md ├── LICENSE ├── README.md ├── UNINSTALL.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── main │ ├── java │ │ └── org │ │ │ └── minimallycorrect │ │ │ └── tickthreading │ │ │ ├── collection │ │ │ ├── LongHashMap.java │ │ │ ├── LongList.java │ │ │ └── LongSet.java │ │ │ ├── concurrent │ │ │ ├── collection │ │ │ │ ├── ConcurrentIterableArrayList.java │ │ │ │ ├── ConcurrentUnsafeIterableArrayList.java │ │ │ │ └── TreeHashSet.java │ │ │ ├── lock │ │ │ │ ├── FIFOMutex.java │ │ │ │ ├── NotReallyAMutex.java │ │ │ │ ├── SimpleMutex.java │ │ │ │ ├── SpinLockMutex.java │ │ │ │ └── UpgradeableReadWriteLock.java │ │ │ ├── scheduling │ │ │ │ ├── ThreadManager.java │ │ │ │ ├── TryRunnable.java │ │ │ │ └── WorldRunnable.java │ │ │ └── threadlocal │ │ │ │ ├── BooleanThreadLocalDefaultFalse.java │ │ │ │ └── CounterThreadLocalAssumeZero.java │ │ │ ├── config │ │ │ └── Config.java │ │ │ ├── exception │ │ │ ├── ConcurrencyError.java │ │ │ ├── ThisIsNotAnError.java │ │ │ └── ThreadStuckError.java │ │ │ ├── log │ │ │ ├── Log.java │ │ │ └── LogFormatter.java │ │ │ ├── mixin │ │ │ ├── extended │ │ │ │ ├── forge │ │ │ │ │ ├── MixinDimensionManager.java │ │ │ │ │ └── MixinOreDictionary.java │ │ │ │ ├── package-info.java │ │ │ │ ├── server │ │ │ │ │ └── MixinMinecraftServer.java │ │ │ │ └── world │ │ │ │ │ ├── MixinWorld.java │ │ │ │ │ └── MixinWorldServer.java │ │ │ ├── forge │ │ │ │ └── MixinFMLCommonHandler.java │ │ │ ├── package-info.java │ │ │ └── world │ │ │ │ ├── MixinChunk.java │ │ │ │ ├── MixinChunkProviderServer.java │ │ │ │ ├── MixinWorld.java │ │ │ │ └── MixinWorldEntitySpawner.java │ │ │ ├── mod │ │ │ ├── TickThreading.java │ │ │ └── TickThreadingCore.java │ │ │ ├── reporting │ │ │ └── LeakDetector.java │ │ │ └── util │ │ │ ├── EnvironmentInfo.java │ │ │ ├── PropertyUtil.java │ │ │ ├── ReflectUtil.java │ │ │ ├── Version.java │ │ │ └── unsafe │ │ │ ├── UnsafeAccess.java │ │ │ └── UnsafeUtil.java │ └── resources │ │ ├── mcmod.info │ │ └── patches │ │ ├── minecraft-extended.xml │ │ └── minecraft.xml └── test │ └── java │ └── org │ └── minimallycorrect │ └── tickthreading │ └── unsafe │ └── UnsafeUtilTest.java └── version.properties /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = tab 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle line endings automatically for files detected as text 2 | # and leave all files detected as binary untouched. 3 | * text=auto eol=lf 4 | 5 | *.bat text eol=crlf 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | # These files are text and should be normalized (Convert crlf => lf) 11 | *.css text 12 | *.df text 13 | *.htm text 14 | *.html text 15 | *.java text 16 | *.js text 17 | *.json text 18 | *.jsp text 19 | *.jspf text 20 | *.properties text 21 | *.sh text 22 | *.sql text 23 | *.svg text 24 | *.tld text 25 | *.txt text 26 | *.xml text 27 | 28 | # These files are binary and should be left untouched 29 | # (binary is a macro for -text -diff) 30 | *.class binary 31 | *.dll binary 32 | *.ear binary 33 | *.gif binary 34 | *.ico binary 35 | *.jar binary 36 | *.jpg binary 37 | *.jpeg binary 38 | *.png binary 39 | *.so binary 40 | *.war binary 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build # 2 | MANIFEST.MF 3 | dependency-reduced-pom.xml 4 | generated 5 | 6 | # Build scripts for testing using hardcoded patchs # 7 | build.sh 8 | refresh.sh 9 | 10 | # Compiled 11 | bin 12 | build 13 | dist 14 | lib 15 | out 16 | run 17 | classes/ 18 | target 19 | *.com 20 | *.class 21 | *.dll 22 | *.exe 23 | *.o 24 | *.so 25 | 26 | # Databases 27 | *.db 28 | *.sql 29 | *.sqlite 30 | 31 | # Packages 32 | *.7z 33 | *.dmg 34 | *.gz 35 | *.iso 36 | *.rar 37 | *.tar 38 | *.zip 39 | 40 | # Repository 41 | .git 42 | 43 | # Logging 44 | /logs 45 | *.log 46 | 47 | # Misc 48 | *.bak 49 | 50 | # System 51 | .DS_Store 52 | ehthumbs.db 53 | Thumbs.db 54 | 55 | # Project 56 | .classpath 57 | .externalToolBuilders 58 | .gradle 59 | .idea 60 | .project 61 | .settings 62 | eclipse 63 | nbproject 64 | atlassian-ide-plugin.xml 65 | build.xml 66 | nb-configuration.xml 67 | *.iml 68 | *.ipr 69 | *.iws 70 | -------------------------------------------------------------------------------- /IMPLEMENTATION.md: -------------------------------------------------------------------------------- 1 | Concurrency model 2 | ==== 3 | 4 | In vanilla, a single thread ticks everything in order. The worlds, entites in the worlds, processes packets, and then repeats. This must be done in less than 50ms. 5 | 6 | For compatibility reasons, TT still sticks with this model - one world can not run ticks ahead of another. 7 | 8 | Each tick, first the pre server tick is handled by all mods with tick handlers. This is not performed concurrently. 9 | 10 | Then, all worlds are ticked. Each world is ticked in a separate thread, up to a maximum of the number of threads set in the config. 11 | 12 | Each world is split up into regions of 32x32 blocks, and a list of these regions is made. The number of threads set in the config are used to tick these regions. The entities and Tile Entities in these regions are then ticked, per region. A region will not be ticked at the same time as an adjacent region. 13 | 14 | The world tick waits on completion of this until moving on to do other tasks. Waiting for completion can be disabled, but increases the risk of bugs, as then chunk unloading can occur while tile/entities are being ticked. 15 | 16 | The server thread waits for all world ticks to complete, runs any mod post-server ticks, then processes all received packets. This then repeats from the start. 17 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | Installing TickThreading 2 | ========== 3 | 4 | - Download TickThreading 5 | - Place it in your mods folder 6 | - Have fun. Make sure to report any issues encountered while using TT to me, not other mod developers! 7 | 8 | Updating TickThreading 9 | ========== 10 | 11 | - Stop the server. 12 | - Download the new TT jar and replace the old one in your mods folder 13 | - Start server. 14 | 15 | Java Tuning 16 | ========== 17 | 18 | TODO: Determine new suggested JVM args. We may now suggest G1GC on java 8? 19 | 20 | - Use the latest Java 8 21 | - Make sure not to set -Xmx higher than you need. Don't set Xmx >= 31GB, as it will disable CompressedOOPs. 22 | - Suggested java parameters - make sure to set -Xmx: 23 | java -server -Xmx{memoryToUseInGB}G -XX:UseSSE=4 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:+UseParNewGC -XX:+DisableExplicitGC -XX:+AggressiveOpts -jar server.jar nogui 24 | 25 | - *Xmx* sets the memory the server is allowed to use. It should be set to a reasonable value - enough for the server to run, but not much higher than it needs. >= 32GB will prevent the JVM from using 26 | compressed OOPs, an optimisation which reduces the size of pointers and generally gives a reasonable performance boost, so it's a good idea to keep Xmx low. 27 | - *UseConcMarkSweepGC* enables the CMS garbage collector. The garbage collector is an important part of the JVM which frees memory used by objects which are no longer needed. By default, 28 | a non-concurrent garbage collector is used, which can cause lag spikes if -Xmx is not very small. 29 | - *UseCMSCompactAtFullCollection* asks the CMS garbage collector to compact, or defragment, the memory used by the JVM at full collections. When disabled, GC might need to run more often if a large 30 | object is allocated which does not fit into any gaps in the heap. 31 | - *UseParNewGC* enables concurrent garbage collection for recently created objects, and should be used for the same reasons as for UseConcMarkSweepGC 32 | - *DisableExplicitGC* prevents silly mods/plugins from asking the JVM to immediately do a full garbage collection. Otherwise, when a mod/plugin does this, it will always freeze the entire server to perform the 33 | garbage collection, ignoring the parameters set above. 34 | - *AggressiveOpts* enables experimental JVM options which should improve performance, but may not have been fully tested and could make performance worse in some cases. For example, with the latest Java 7 35 | it enables some optimisations relating to auto boxing - a conversion between primitive and object types. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 nallar (Ross Allan) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TickThreading was a multi-threaded modded minecraft server, implemented as a set of patches on top of either forge or forge + craftbukkit, with working versions for 1.4.7 through 1.6.4. 2 | The largest pack it worked well with was the original FTB Ultimate pack for 1.4.7. 3 | The approach had a huge maintenance burden, requiring constant reporting of issues and manually creating patches, not at all sustainable as a hobby project. I had too much free time when I was supposed to be in secondary school. 4 | It's unlikely this project will ever resume. Without a way to statically find issues this approach is doomed to be entirely too iterative and time-consuming. 5 | I'm glad some people found it useful for the versions it worked for. 6 | 7 | If you want to do something with the earlier working versions check out the [1.4.7](https://github.com/MinimallyCorrect/TickThreading/tree/1.4.7)/[1.5.2](https://github.com/MinimallyCorrect/TickThreading/tree/1.5.2) branches. This unfinished 1.12 branch is not a full implementation. 8 | 9 | Luna 10 | 11 | --- 12 | 13 | TickThreading [![Discord](https://img.shields.io/discord/313371711632441344.svg)](https://discordapp.com/invite/YrV3bDm) [![Build Status](https://jenkins.nallar.me/job/TickThreading/branch/1.12/badge/icon)](https://jenkins.nallar.me/job/TickThreading/branch/1.12/) 14 | ========== 15 | Multi-threaded minecraft. Requires Forge. 16 | 17 | TickThreading is licensed under the MIT license 18 | 19 | Download 20 | ----- 21 | Download the latest builds from [Jenkins](https://jenkins.nallar.me/jobs/TickThreading). 22 | 23 | Support [![Discord](https://img.shields.io/discord/313371711632441344.svg)](https://discordapp.com/invite/YrV3bDm) 24 | ---------------------------------- 25 | [Chat with us on discord.](https://discordapp.com/invite/YrV3bDm) 26 | 27 | Compatibility with other mods 28 | ----- 29 | [See the wiki.](https://github.com/nallar/TickThreading/wiki/Mod-Compatibility) 30 | 31 | Configuration 32 | ----- 33 | TickThreading uses minecraft forge's suggested config location - minecraft folder/configs/TickThreading.cfg 34 | Some additional configuration options which need to be set before the server is started can be changed in the ttlaunch.properties file in your server folder. 35 | It's commented quite well, and is hopefully understandable. If any of the descriptions don't make sense please make an issue. 36 | 37 | Logging 38 | ----- 39 | TickThreading stores its logs in the TickThreadingLogs directory, and will keep the previous 5 logs. 40 | Make sure to include all relevant logs if you run into a problem. 41 | 42 | Compiling 43 | --------- 44 | TickThreading is built using Gradle. 45 | 46 | * Install JDK 8. Set your JDK_HOME environment variable to your JDK 8 install path 47 | * Checkout this repo and run: `gradlew.bat` 48 | 49 | Coding and Pull Request Formatting 50 | ---------------------------------- 51 | * Generally follows the Oracle coding standards. 52 | * Tabs, no spaces. 53 | * Pull requests must compile and work. 54 | * Pull requests must be formatted properly. 55 | * Code should be self-documenting - when possible meaningful names and good design should make comments unnecessary 56 | 57 | Please follow the above conventions if you want your pull requests accepted. 58 | 59 | Acknowledgements 60 | ---------------------------------- 61 | 62 | YourKit is kindly supporting open source projects with its full-featured Java Profiler. YourKit, LLC is the creator of innovative and intelligent tools for profiling Java and .NET applications. Take a look at YourKit's leading software products: [YourKit Java Profiler](http://www.yourkit.com/java/profiler/index.jsp) and [YourKit .NET Profiler](http://www.yourkit.com/.net/profiler/index.jsp). 63 | -------------------------------------------------------------------------------- /UNINSTALL.md: -------------------------------------------------------------------------------- 1 | Uninstalling TickThreading 2 | ========== 3 | 4 | - Delete TickThreading.jar 5 | - Revert start script changes (launch minecraftforge/MCPC+ jar as you did before installing TT) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { url "https://repo.nallar.me/" } 5 | maven { url "https://plugins.gradle.org/m2/" } 6 | maven { url "http://files.minecraftforge.net/maven" } 7 | } 8 | dependencies { 9 | classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' 10 | classpath 'org.minimallycorrect.modpatcher:ModPatcherGradle:0.1-SNAPSHOT' 11 | classpath 'org.minimallycorrect.libloader:LibLoaderGradle:0.1-SNAPSHOT' 12 | classpath 'org.minimallycorrect.gradle:DefaultsPlugin:0.0.28' 13 | } 14 | } 15 | apply plugin: 'org.minimallycorrect.gradle.DefaultsPlugin' 16 | apply plugin: 'org.minimallycorrect.modpatcher.ModPatcherGradle' 17 | apply plugin: 'org.minimallycorrect.libloader.LibLoaderGradle' 18 | 19 | group = 'org.minimallycorrect.tickthreading' 20 | 21 | minimallyCorrectDefaults { 22 | minecraft = 1.12 23 | fmlCorePlugin = "org.minimallycorrect.tickthreading.mod.TickThreadingCore" 24 | fmlCorePluginContainsFmlMod = true 25 | labels = ['minecraft-mod', 'threading', 'minecraft', 'java', 'performance'] 26 | description = "Multi-threaded minecraft. Performance over correctness. What could go wrong?" 27 | } () 28 | 29 | gradle.startParameter.showStacktrace = org.gradle.api.logging.configuration.ShowStacktrace.ALWAYS 30 | 31 | modpatcher { 32 | mixinPackage = "org.minimallycorrect.tickthreading.mixin" 33 | extractGeneratedSources = true 34 | generateInheritanceHierarchy = true 35 | } 36 | 37 | sourceCompatibility = 1.8 38 | targetCompatibility = 1.8 39 | 40 | dependencies { 41 | testCompile 'junit:junit:4.12' 42 | compileOnly 'org.jetbrains:annotations:15.0' 43 | testCompileOnly 'org.projectlombok:lombok:1.16.18' 44 | testCompileOnly 'org.jetbrains:annotations:15.0' 45 | libLoader "org.minimallycorrect.typedconfig:TypedConfig:0.1-SNAPSHOT" 46 | libLoader "org.minimallycorrect.modpatcher:ModPatcher:${minimallyCorrectDefaults.minecraft}-SNAPSHOT" 47 | } 48 | 49 | if (System.env.GRADLE_USER_HOME) { 50 | ext.homeDir = System.env.GRADLE_USER_HOME + '/' 51 | } else { 52 | ext.homeDir = System.properties['user.home'] + '/.gradle/' 53 | } 54 | ext.mappingsPath = homeDir + 'caches/minecraft/net/minecraftforge/forge/' + minimallyCorrectDefaults.forge + '/unpacked/conf/' 55 | 56 | def jarConfig = { 57 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 58 | from mappingsPath + 'packaged.srg' 59 | from mappingsPath + 'methods.csv' 60 | from mappingsPath + 'fields.csv' 61 | from './generated/extendsMap.obj' 62 | } 63 | 64 | jar jarConfig 65 | 66 | jar { 67 | classifier = 'extended' 68 | } 69 | 70 | def outputJarFile = (File) jar.outputs.files.getSingleFile() 71 | 72 | task coreJar(type: Zip, dependsOn: reobfJar) { 73 | from zipTree(outputJarFile).matching { exclude 'org/minimallycorrect/tickthreading/mixin/extended/**' } 74 | archiveName = jar.archiveName.replace('-' + jar.classifier, "-core") 75 | destinationDir = jar.destinationDir 76 | exclude('org/minimallycorrect/tickthreading/mixin/extended**') 77 | } 78 | 79 | artifacts { 80 | archives coreJar 81 | } 82 | 83 | build.dependsOn(coreJar) 84 | 85 | // Source compiler configuration 86 | tasks.withType(JavaCompile) { 87 | sourceCompatibility = 8 88 | targetCompatibility = 8 89 | options.with { 90 | deprecation = true 91 | encoding = 'UTF-8' 92 | compilerArgs << 93 | "-XDignore.symbol.file=true" << 94 | "-Xlint:all" << 95 | "-Xlint:-path" << 96 | "-Xlint:-processing" 97 | fork = true 98 | forkOptions.executable = 'javac' 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # ForgeGradle leaks JarFile instances due to https://github.com/md-5/SpecialSource/pull/44 2 | org.gradle.daemon=false 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinimallyCorrect/TickThreading/ef6fcce3fb91373fd26bd75ff4060c7047f09d4d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="-Xmx2G" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS=-Xmx2G 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'TickThreading' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/collection/LongHashMap.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.collection; 2 | 3 | import java.util.*; 4 | 5 | @SuppressWarnings("unchecked") 6 | public class LongHashMap { 7 | private static final long EMPTY_KEY = Long.MIN_VALUE; 8 | private static final int BUCKET_SIZE = 8192; 9 | private final long[][] keys = new long[BUCKET_SIZE][]; 10 | private final java.lang.Object[][] values = new java.lang.Object[BUCKET_SIZE][]; 11 | private int size; 12 | 13 | private static long keyIndex(long key) { 14 | key ^= key >>> 33; 15 | key *= 0xff51afd7ed558ccdL; 16 | key ^= key >>> 33; 17 | key *= 0xc4ceb9fe1a85ec53L; 18 | key ^= key >>> 33; 19 | return key; 20 | } 21 | 22 | public long[][] getKeys() { 23 | return keys; 24 | } 25 | 26 | public int getNumHashElements() { 27 | return size; 28 | } 29 | 30 | public boolean containsItem(long key) { 31 | return getValueByKey(key) != null; 32 | } 33 | 34 | public T getValueByKey(long key) { 35 | int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1)); 36 | long[] inner = keys[index]; 37 | if (inner == null) { 38 | return null; 39 | } 40 | 41 | for (int i = 0; i < inner.length; i++) { 42 | long innerKey = inner[i]; 43 | if (innerKey == EMPTY_KEY) { 44 | return null; 45 | } else if (innerKey == key) { 46 | java.lang.Object[] value = values[index]; 47 | if (value != null) { 48 | return (T) value[i]; 49 | } 50 | } 51 | } 52 | 53 | return null; 54 | } 55 | 56 | public void add(long key, T value) { 57 | put(key, value); 58 | } 59 | 60 | public synchronized Object put(long key, T value) { 61 | int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1)); 62 | long[] innerKeys = keys[index]; 63 | java.lang.Object[] innerValues = values[index]; 64 | 65 | if (innerKeys == null) { 66 | // need to make a new chain 67 | keys[index] = innerKeys = new long[8]; 68 | Arrays.fill(innerKeys, EMPTY_KEY); 69 | values[index] = innerValues = new java.lang.Object[8]; 70 | innerKeys[0] = key; 71 | innerValues[0] = value; 72 | size++; 73 | } else { 74 | int i; 75 | for (i = 0; i < innerKeys.length; i++) { 76 | // found an empty spot in the chain to put this 77 | long currentKey = innerKeys[i]; 78 | if (currentKey == EMPTY_KEY) { 79 | size++; 80 | } 81 | if (currentKey == EMPTY_KEY || currentKey == key) { 82 | java.lang.Object old = innerValues[i]; 83 | innerKeys[i] = key; 84 | innerValues[i] = value; 85 | return old; 86 | } 87 | } 88 | 89 | // chain is full, resize it and add our new entry 90 | keys[index] = innerKeys = Arrays.copyOf(innerKeys, i << 1); 91 | Arrays.fill(innerKeys, i, innerKeys.length, EMPTY_KEY); 92 | values[index] = innerValues = Arrays.copyOf(innerValues, i << 1); 93 | innerKeys[i] = key; 94 | innerValues[i] = value; 95 | size++; 96 | } 97 | return null; 98 | } 99 | 100 | public synchronized T remove(long key) { 101 | int index = (int) (keyIndex(key) & (BUCKET_SIZE - 1)); 102 | long[] inner = keys[index]; 103 | if (inner == null) { 104 | return null; 105 | } 106 | 107 | for (int i = 0; i < inner.length; i++) { 108 | // hit the end of the chain, didn't find this entry 109 | if (inner[i] == EMPTY_KEY) { 110 | break; 111 | } 112 | 113 | if (inner[i] == key) { 114 | java.lang.Object value = values[index][i]; 115 | 116 | for (i++; i < inner.length; i++) { 117 | if (inner[i] == EMPTY_KEY) { 118 | break; 119 | } 120 | 121 | inner[i - 1] = inner[i]; 122 | values[index][i - 1] = values[index][i]; 123 | } 124 | 125 | inner[i - 1] = EMPTY_KEY; 126 | values[index][i - 1] = null; 127 | size--; 128 | return (T) value; 129 | } 130 | } 131 | 132 | return null; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/collection/LongList.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.collection; 2 | 3 | import java.util.*; 4 | 5 | public class LongList { 6 | private static final long[] EMPTY_LIST = new long[0]; 7 | public int size = 0; 8 | public long[] list; 9 | 10 | public LongList() { 11 | list = EMPTY_LIST; 12 | } 13 | 14 | public LongList(int initialSize) { 15 | list = new long[initialSize]; 16 | } 17 | 18 | public void add(long value) { 19 | int index = size++; 20 | long[] list = this.list; 21 | int length = list.length; 22 | if (index >= length) { 23 | this.list = list = Arrays.copyOf(list, length == 0 ? 10 : length * 2); 24 | } 25 | list[index] = value; 26 | } 27 | 28 | public void set(int index, long value) { 29 | if (index >= size) { 30 | throw new ArrayIndexOutOfBoundsException(index + " > " + size); 31 | } 32 | list[index] = value; 33 | } 34 | 35 | public long get(int index) { 36 | if (index >= size) { 37 | throw new ArrayIndexOutOfBoundsException(index + " > " + size); 38 | } 39 | return list[index]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/collection/LongSet.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.collection; 2 | 3 | import java.util.*; 4 | 5 | public class LongSet extends AbstractSet implements Set { 6 | private static final Object set = new Object(); 7 | private final LongHashMap m = new LongHashMap<>(); 8 | 9 | public boolean add(long l) { 10 | return m.put(l, set) == null; 11 | } 12 | 13 | public boolean contains(long l) { 14 | return m.containsItem(l); 15 | } 16 | 17 | @Override 18 | public boolean contains(Object l) { 19 | return m.containsItem((Long) l); 20 | } 21 | 22 | @Override 23 | public boolean remove(Object l) { 24 | return m.remove((Long) l) != null; 25 | } 26 | 27 | public boolean remove(long l) { 28 | return m.remove(l) != null; 29 | } 30 | 31 | @Override 32 | public LongIterator iterator() { 33 | return new LongIterator(m.getKeys()); 34 | } 35 | 36 | @Override 37 | public int size() { 38 | return 0; 39 | } 40 | 41 | public static final class LongIterator implements Iterator { 42 | private static final long EMPTY_KEY = Long.MIN_VALUE; 43 | private final long[][] keys; 44 | private long nextKey = EMPTY_KEY; 45 | private int outerIndex = 0; 46 | private int innerIndex = 0; 47 | 48 | public LongIterator(final long[][] keys) { 49 | this.keys = keys; 50 | nextLong(); 51 | } 52 | 53 | @Override 54 | public boolean hasNext() { 55 | return nextKey != EMPTY_KEY; 56 | } 57 | 58 | @Deprecated 59 | @Override 60 | public Long next() { 61 | return nextLong(); 62 | } 63 | 64 | public long nextLong() { 65 | long thisKey = this.nextKey; 66 | if (thisKey == EMPTY_KEY) { 67 | throw new NoSuchElementException(); 68 | } 69 | long searchingKey = EMPTY_KEY; 70 | for (; outerIndex < keys.length; outerIndex++) { 71 | long[] keys = this.keys[outerIndex]; 72 | if (keys == null) { 73 | innerIndex = 0; 74 | continue; 75 | } 76 | for (; innerIndex < keys.length; innerIndex++) { 77 | searchingKey = keys[innerIndex]; 78 | if (searchingKey == EMPTY_KEY) { 79 | innerIndex = 0; 80 | break; 81 | } 82 | } 83 | } 84 | this.nextKey = searchingKey; 85 | return thisKey; 86 | } 87 | 88 | @Override 89 | public void remove() { 90 | throw new UnsupportedOperationException(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/collection/ConcurrentIterableArrayList.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.collection; 2 | 3 | import java.util.*; 4 | 5 | public class ConcurrentIterableArrayList extends ArrayList { 6 | private static final long serialVersionUID = 0; 7 | private int index; 8 | 9 | public synchronized void reset() { 10 | index = 0; 11 | } 12 | 13 | public synchronized T next() { 14 | return index < size() ? this.get(index++) : null; 15 | } 16 | 17 | @Override 18 | public synchronized T remove(int index) { 19 | if (index < this.index) { 20 | this.index--; 21 | } 22 | return super.remove(index); 23 | } 24 | 25 | @Override 26 | public boolean remove(Object o) { 27 | for (int index = 0; index < size(); index++) { 28 | if (o.equals(get(index))) { 29 | remove(index); 30 | return true; 31 | } 32 | } 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/collection/ConcurrentUnsafeIterableArrayList.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.collection; 2 | 3 | import java.util.*; 4 | 5 | import sun.misc.Unsafe; 6 | 7 | import org.minimallycorrect.tickthreading.util.ReflectUtil; 8 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeAccess; 9 | 10 | @SuppressWarnings({"unchecked", "rawtypes"}) 11 | public class ConcurrentUnsafeIterableArrayList extends ArrayList { 12 | private static final long serialVersionUID = 0; 13 | private static final Unsafe $ = UnsafeAccess.$; 14 | private static final long elementDataIndex = $.objectFieldOffset(ReflectUtil.getField(ArrayList.class, "elementData")); 15 | 16 | public ConcurrentUnsafeIterableArrayList(final int initialCapacity) { 17 | super(initialCapacity); 18 | } 19 | 20 | public ConcurrentUnsafeIterableArrayList() { 21 | super(); 22 | } 23 | 24 | public ConcurrentUnsafeIterableArrayList(final Collection c) { 25 | super(c); 26 | } 27 | 28 | public Object[] elementData() { 29 | return (Object[]) $.getObject(this, elementDataIndex); 30 | } 31 | 32 | public java.util.Iterator unsafeIterator() { 33 | return new Iterator(elementData(), size()); 34 | } 35 | 36 | public Iterable unsafe() { 37 | return this::unsafeIterator; 38 | } 39 | 40 | private static class Iterator implements java.util.Iterator { 41 | final Object[] elementData; 42 | int index; 43 | private Object next; 44 | 45 | Iterator(final Object[] elementData, int start) { 46 | this.elementData = elementData; 47 | if (start >= elementData.length) { 48 | start = elementData.length - 1; 49 | } 50 | index = start; 51 | getNext(); 52 | } 53 | 54 | @SuppressWarnings("FieldRepeatedlyAccessedInMethod") 55 | private Object getNext() { 56 | Object l = next; 57 | Object o; 58 | int i = index; 59 | if (i == -1) { 60 | next = null; 61 | } else { 62 | do { 63 | o = elementData[i]; 64 | } while (--i != -1 && (o == null || o == l)); 65 | index = i; 66 | next = o; 67 | } 68 | return l; 69 | } 70 | 71 | @Override 72 | public boolean hasNext() { 73 | return next != null; 74 | } 75 | 76 | @Override 77 | public T next() { 78 | Object o = getNext(); 79 | if (o == null) { 80 | throw new IllegalStateException("Must call hasNext before next"); 81 | } 82 | return (T) o; 83 | } 84 | 85 | @Override 86 | public void remove() { 87 | throw new UnsupportedOperationException(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/collection/TreeHashSet.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.collection; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | 6 | @SuppressWarnings({"unchecked", "rawtypes"}) 7 | public class TreeHashSet extends TreeSet { 8 | private static final long serialVersionUID = 0; 9 | private final Set internalHashSet = Collections.newSetFromMap(new ConcurrentHashMap()); 10 | 11 | public Iterator concurrentIterator() { 12 | return internalHashSet.iterator(); 13 | } 14 | 15 | @Override 16 | public boolean contains(Object o) { 17 | return internalHashSet.contains(o); 18 | } 19 | 20 | @Override 21 | public synchronized boolean add(T o) { 22 | if (internalHashSet.add(o)) { 23 | super.add(o); 24 | return true; 25 | } 26 | return false; 27 | } 28 | 29 | @Override 30 | public synchronized boolean remove(Object o) { 31 | if (internalHashSet.remove(o)) { 32 | super.remove(o); 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | @Override 39 | public synchronized void clear() { 40 | super.clear(); 41 | internalHashSet.clear(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/lock/FIFOMutex.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.lock; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | import java.util.concurrent.atomic.*; 6 | import java.util.concurrent.locks.*; 7 | 8 | public final class FIFOMutex implements Lock { 9 | private final AtomicBoolean locked = new AtomicBoolean(false); 10 | private final Queue waiters = new ConcurrentLinkedQueue<>(); 11 | 12 | @Override 13 | public void lock() { 14 | boolean wasInterrupted = false; 15 | Thread current = Thread.currentThread(); 16 | waiters.add(current); 17 | 18 | while (waiters.peek() != current || !locked.compareAndSet(false, true)) { 19 | LockSupport.park(); 20 | if (Thread.interrupted()) { 21 | wasInterrupted = true; 22 | } 23 | } 24 | 25 | waiters.remove(); 26 | if (wasInterrupted) { 27 | current.interrupt(); 28 | } 29 | } 30 | 31 | @Override 32 | public void lockInterruptibly() throws InterruptedException { 33 | throw new UnsupportedOperationException(); 34 | } 35 | 36 | @Override 37 | public boolean tryLock() { 38 | throw new UnsupportedOperationException(); 39 | } 40 | 41 | @Override 42 | public void unlock() { 43 | locked.set(false); 44 | LockSupport.unpark(waiters.peek()); 45 | } 46 | 47 | @Override 48 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 49 | throw new UnsupportedOperationException(); 50 | } 51 | 52 | @Override 53 | public Condition newCondition() { 54 | throw new UnsupportedOperationException(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/lock/NotReallyAMutex.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.lock; 2 | 3 | import java.util.concurrent.*; 4 | import java.util.concurrent.locks.*; 5 | 6 | public final class NotReallyAMutex implements Lock { 7 | public static final NotReallyAMutex lock = new NotReallyAMutex(); 8 | 9 | @Override 10 | public void lockInterruptibly() throws InterruptedException {} 11 | 12 | @Override 13 | public void lock() {} 14 | 15 | @Override 16 | public void unlock() {} 17 | 18 | @Override 19 | public boolean tryLock() { 20 | return true; 21 | } 22 | 23 | @Override 24 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 25 | return true; 26 | } 27 | 28 | @Override 29 | public Condition newCondition() { 30 | throw new UnsupportedOperationException(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/lock/SimpleMutex.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.lock; 2 | 3 | import java.util.concurrent.*; 4 | import java.util.concurrent.locks.*; 5 | 6 | public final class SimpleMutex implements Lock { 7 | private boolean locked = false; 8 | 9 | @Override 10 | public synchronized void lock() { 11 | while (locked) { 12 | try { 13 | wait(); 14 | } catch (InterruptedException ignored) {} 15 | } 16 | locked = true; 17 | } 18 | 19 | @Override 20 | public synchronized void unlock() { 21 | locked = false; 22 | notify(); 23 | } 24 | 25 | @Override 26 | public void lockInterruptibly() throws InterruptedException { 27 | throw new UnsupportedOperationException(); 28 | } 29 | 30 | @Override 31 | public synchronized boolean tryLock() { 32 | if (!locked) { 33 | locked = true; 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | @Override 40 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 41 | throw new UnsupportedOperationException(); 42 | } 43 | 44 | @Override 45 | public Condition newCondition() { 46 | throw new UnsupportedOperationException(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/lock/SpinLockMutex.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.lock; 2 | 3 | import java.util.concurrent.*; 4 | import java.util.concurrent.locks.*; 5 | 6 | import sun.misc.Unsafe; 7 | 8 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeAccess; 9 | 10 | public final class SpinLockMutex implements Lock { 11 | private static final Unsafe $ = UnsafeAccess.$; 12 | private static final long index = $.objectFieldOffset(SpinLockMutex.class.getFields()[0]); 13 | private volatile int locked = 0; 14 | 15 | @Override 16 | public synchronized void lock() { 17 | //noinspection StatementWithEmptyBody 18 | while (!$.compareAndSwapInt(this, index, 0, 1)) { 19 | // Spin lock. 20 | // TODO: Could we instead work on something else here? 21 | // Avoids overhead of OS scheduling without doing nothing 22 | // might not be worth the effort to get it working correctly 23 | } 24 | } 25 | 26 | @Override 27 | public synchronized void unlock() { 28 | if (locked == 0) { 29 | throw new IllegalStateException("Unlocked " + this + " before it was locked."); 30 | } 31 | locked = 0; 32 | } 33 | 34 | @Override 35 | public synchronized boolean tryLock() { 36 | return $.compareAndSwapInt(this, index, 0, 1); 37 | } 38 | 39 | @Override 40 | public synchronized void lockInterruptibly() throws InterruptedException { 41 | throw new UnsupportedOperationException(); 42 | } 43 | 44 | @Override 45 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 46 | throw new UnsupportedOperationException(); 47 | } 48 | 49 | @Override 50 | public Condition newCondition() { 51 | throw new UnsupportedOperationException(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/lock/UpgradeableReadWriteLock.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.lock; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | import java.util.concurrent.locks.*; 6 | 7 | public class UpgradeableReadWriteLock implements ReadWriteLock { 8 | private final Map readingThreads = new HashMap<>(); 9 | private int writeAccesses = 0; 10 | private int writeRequests = 0; 11 | private int readRequests = 0; 12 | private Thread writingThread = null; 13 | private final Lock readLock = new SimpleLock() { 14 | @Override 15 | public void lock() { 16 | UpgradeableReadWriteLock.this.lockRead(); 17 | } 18 | 19 | @Override 20 | public void unlock() { 21 | UpgradeableReadWriteLock.this.unlockRead(); 22 | } 23 | }; 24 | private final Lock writeLock = new SimpleLock() { 25 | @Override 26 | public void lock() { 27 | UpgradeableReadWriteLock.this.lockWrite(); 28 | } 29 | 30 | @Override 31 | public void unlock() { 32 | UpgradeableReadWriteLock.this.unlockWrite(); 33 | } 34 | }; 35 | 36 | @Override 37 | public Lock readLock() { 38 | return readLock; 39 | } 40 | 41 | @Override 42 | public Lock writeLock() { 43 | return writeLock; 44 | } 45 | 46 | public final synchronized void lockRead() { 47 | Thread callingThread = Thread.currentThread(); 48 | readRequests++; 49 | while (writingThread != callingThread && writingThread != null) { 50 | try { 51 | wait(); 52 | } catch (InterruptedException ignored) {} 53 | } 54 | readRequests--; 55 | 56 | Integer count = readingThreads.get(callingThread); 57 | readingThreads.put(callingThread, count == null ? 1 : count + 1); 58 | } 59 | 60 | public final synchronized void unlockRead() { 61 | Thread callingThread = Thread.currentThread(); 62 | Integer accessCount_ = readingThreads.get(callingThread); 63 | if (accessCount_ == null) { 64 | throw new IllegalMonitorStateException("Calling Thread does not" + 65 | " hold a read lock on this ReadWriteLock"); 66 | } 67 | if (accessCount_ == 1) { 68 | readingThreads.remove(callingThread); 69 | if (writeRequests > 0 && readingThreads.isEmpty()) { 70 | notify(); 71 | } 72 | } else { 73 | readingThreads.put(callingThread, (accessCount_ - 1)); 74 | } 75 | } 76 | 77 | @SuppressWarnings("FieldRepeatedlyAccessedInMethod") 78 | public final synchronized void lockWrite() { 79 | writeRequests++; 80 | Thread callingThread = Thread.currentThread(); 81 | int size; 82 | while ((writingThread != callingThread && writingThread != null) || ((size = readingThreads.size()) > 1 || (size == 1 && readingThreads.get(callingThread) == null))) { 83 | try { 84 | wait(); 85 | } catch (InterruptedException ignored) {} 86 | } 87 | writeRequests--; 88 | writeAccesses++; 89 | writingThread = callingThread; 90 | } 91 | 92 | public final synchronized void unlockWrite() { 93 | if (writingThread != Thread.currentThread()) { 94 | throw new IllegalMonitorStateException("Calling Thread does not" + 95 | " hold the write lock on this ReadWriteLock"); 96 | } 97 | writeAccesses--; 98 | if (writeAccesses == 0) { 99 | writingThread = null; 100 | } 101 | if (writeRequests > 0 || readRequests > 0) { 102 | notifyAll(); 103 | } 104 | } 105 | 106 | private abstract static class SimpleLock implements Lock { 107 | SimpleLock() {} 108 | 109 | @Override 110 | public void lockInterruptibly() throws InterruptedException { 111 | lock(); 112 | } 113 | 114 | @Override 115 | public boolean tryLock() { 116 | throw new UnsupportedOperationException("TwoWayReentrantReadWriteLock doesn't support this."); 117 | } 118 | 119 | @Override 120 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 121 | throw new UnsupportedOperationException("TwoWayReentrantReadWriteLock doesn't support this."); 122 | } 123 | 124 | @Override 125 | public Condition newCondition() { 126 | throw new UnsupportedOperationException("TwoWayReentrantReadWriteLock doesn't support this."); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/scheduling/ThreadManager.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.scheduling; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | import java.util.concurrent.atomic.*; 6 | 7 | import org.minimallycorrect.tickthreading.concurrent.collection.ConcurrentIterableArrayList; 8 | import org.minimallycorrect.tickthreading.exception.ThreadStuckError; 9 | import org.minimallycorrect.tickthreading.log.Log; 10 | 11 | /* 12 | TODO 19/02/2016: Rework this class? Do we still need this, or is there a better solution? 13 | 14 | Was used in previous TT versions to split off work to multiple threads then join afterwards. 15 | */ 16 | public final class ThreadManager { 17 | public static final Map threadIdToManager = new ConcurrentHashMap<>(); 18 | private static final Runnable killTask = () -> { 19 | throw new ThreadDeath(); 20 | }; 21 | private final ArrayBlockingQueue taskQueue = new ArrayBlockingQueue<>(100); 22 | private final String name; 23 | private final Set workThreads = new HashSet<>(); 24 | private final Object readyLock = new Object(); 25 | private final AtomicInteger waiting = new AtomicInteger(); 26 | private final ConcurrentLinkedQueue tryRunnableQueue = new ConcurrentLinkedQueue<>(); 27 | private final Runnable workerTask = this::workerTask; 28 | private String parentName; 29 | 30 | public ThreadManager(int threads, String name) { 31 | this.name = name; 32 | //DeadLockDetector.threadManagers.add(this); 33 | addThreads(threads); 34 | } 35 | 36 | private void workerTask() { 37 | try { 38 | while (true) { 39 | try { 40 | Runnable runnable = taskQueue.take(); 41 | if (runnable == killTask) { 42 | workThreads.remove(Thread.currentThread()); 43 | return; 44 | } 45 | try { 46 | runnable.run(); 47 | } catch (ThreadStuckError | ThreadDeath ignored) {} 48 | } catch (InterruptedException ignored) {} catch (Throwable t) { 49 | Log.error("Unhandled exception in worker thread " + Thread.currentThread().getName(), t); 50 | } 51 | if (waiting.decrementAndGet() == 0) { 52 | synchronized (readyLock) { 53 | readyLock.notify(); 54 | } 55 | } 56 | } 57 | } finally { 58 | threadIdToManager.remove(Thread.currentThread().getId()); 59 | } 60 | } 61 | 62 | private void addThread(String name) { 63 | Thread workThread = new Thread(workerTask, name); 64 | workThread.start(); 65 | threadIdToManager.put(workThread.getId(), this.name); 66 | workThreads.add(workThread); 67 | } 68 | 69 | public boolean isWaiting() { 70 | return waiting.get() > 0; 71 | } 72 | 73 | public void waitForCompletion() { 74 | synchronized (readyLock) { 75 | while (waiting.get() > 0) { 76 | try { 77 | readyLock.wait(1L); 78 | } catch (InterruptedException ignored) {} 79 | } 80 | } 81 | } 82 | 83 | public void runDelayableList(final ConcurrentIterableArrayList tasks) { 84 | tasks.reset(); 85 | Runnable arrayRunnable = new DelayableRunnable(tasks, tryRunnableQueue); 86 | for (int i = 0, len = workThreads.size(); i < len; i++) { 87 | run(arrayRunnable); 88 | } 89 | } 90 | 91 | public void run(Iterable tasks) { 92 | for (Runnable runnable : tasks) { 93 | run(runnable); 94 | } 95 | } 96 | 97 | public void runAll(Runnable runnable) { 98 | for (int i = 0; i < size(); i++) { 99 | run(runnable); 100 | } 101 | } 102 | 103 | public void run(Runnable runnable) { 104 | if (taskQueue.add(runnable)) { 105 | waiting.incrementAndGet(); 106 | } else { 107 | Log.error("Failed to add " + runnable); 108 | } 109 | if (parentName == null) { 110 | String pName = threadIdToManager.get(Thread.currentThread().getId()); 111 | parentName = pName == null ? "none" : pName; 112 | } 113 | } 114 | 115 | private void addThreads(int count) { 116 | count += workThreads.size(); 117 | for (int i = workThreads.size() + 1; i <= count; i++) { 118 | addThread(name + " - " + i); 119 | } 120 | } 121 | 122 | private void killThreads(int number) { 123 | for (int i = 0; i < number; i++) { 124 | taskQueue.add(killTask); 125 | } 126 | } 127 | 128 | public int size() { 129 | return workThreads.size(); 130 | } 131 | 132 | public void stop() { 133 | //DeadLockDetector.threadManagers.remove(this); 134 | killThreads(workThreads.size()); 135 | } 136 | 137 | public String getName() { 138 | return name; 139 | } 140 | 141 | public String getParentName() { 142 | return parentName; 143 | } 144 | 145 | private static class DelayableRunnable implements Runnable { 146 | private final ConcurrentIterableArrayList tasks; 147 | private final ConcurrentLinkedQueue tryRunnableQueue; 148 | 149 | public DelayableRunnable(ConcurrentIterableArrayList tasks, ConcurrentLinkedQueue tryRunnableQueue) { 150 | this.tasks = tasks; 151 | this.tryRunnableQueue = tryRunnableQueue; 152 | } 153 | 154 | @Override 155 | public void run() { 156 | TryRunnable r; 157 | while ((r = tasks.next()) != null || (r = tryRunnableQueue.poll()) != null) { 158 | if (!r.run()) { 159 | tryRunnableQueue.add(r); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/scheduling/TryRunnable.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.scheduling; 2 | 3 | public interface TryRunnable { 4 | boolean run(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/scheduling/WorldRunnable.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.scheduling; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.SneakyThrows; 5 | import lombok.val; 6 | 7 | import org.minimallycorrect.tickthreading.config.Config; 8 | import org.minimallycorrect.tickthreading.log.Log; 9 | 10 | import net.minecraft.world.WorldServer; 11 | 12 | @AllArgsConstructor 13 | public class WorldRunnable implements Runnable { 14 | private final WorldServer world; 15 | 16 | @SneakyThrows 17 | @Override 18 | public void run() { 19 | val world = this.world; 20 | val server = world.getMinecraftServer(); 21 | assert server != null; 22 | 23 | long TARGET_TPS = Config.$.targetTps; 24 | long TARGET_TICK_TIME = 1000000000 / TARGET_TPS; 25 | long startTime = System.nanoTime(); 26 | float tickTime = 1; 27 | boolean waitForWorldTick = !Config.$.separatePerWorldTickLoops; 28 | 29 | while (true) { 30 | if (world.unloaded) 31 | return; 32 | if (waitForWorldTick) 33 | server.waitForWorldTick(); 34 | 35 | try { 36 | world.tickWithEvents(); 37 | } catch (Exception e) { 38 | Log.error("Exception in main tick loop", e); 39 | } 40 | 41 | long endTime = System.nanoTime(); 42 | tickTime = tickTime * 0.98f + ((endTime - startTime) * 0.02f); 43 | 44 | long wait = (TARGET_TICK_TIME - (endTime - startTime)) / 1000000; 45 | if (wait > 0 && TARGET_TICK_TIME * TARGET_TPS / tickTime > TARGET_TPS) { 46 | Thread.sleep(wait); 47 | startTime = System.currentTimeMillis(); 48 | } else { 49 | startTime = endTime; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/threadlocal/BooleanThreadLocalDefaultFalse.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.threadlocal; 2 | 3 | import sun.misc.Unsafe; 4 | 5 | import org.minimallycorrect.tickthreading.util.ReflectUtil; 6 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeAccess; 7 | 8 | /** 9 | * Accessing a field is much faster than a normal ThreadLocal get. Here, we maintain a field which counts the number of threads in which the variable is true In most cases, this allows us to avoid the performance hit of a hashmap lookup in ThreadLocal.get(), assuming the variable is normally false in all threads. 10 | */ 11 | public class BooleanThreadLocalDefaultFalse extends ThreadLocal { 12 | private static final Unsafe $ = UnsafeAccess.$; 13 | private static final long index = $.objectFieldOffset(ReflectUtil.getField(BooleanThreadLocalDefaultFalse.class, "count")); 14 | @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "CanBeFinal"}) 15 | private int count = 0; 16 | // Unsafe magics. 17 | 18 | @Override 19 | public Boolean initialValue() { 20 | return false; 21 | } 22 | 23 | /** 24 | * @param value Must be Boolean.TRUE or Boolean.FALSE. No new Boolean(true/false)! Must always set back to false before the thread stops, else performance will be worse. Not going to break anything, but bad for performance. 25 | */ 26 | @Override 27 | public void set(Boolean value) { 28 | getAndSet(value); 29 | } 30 | 31 | /** 32 | * @param value Must be Boolean.TRUE or Boolean.FALSE. No new Boolean(true/false)! Must always set back to false before the thread stops, else performance will be worse. Not going to break anything, but bad for performance. 33 | */ 34 | public Boolean getAndSet(Boolean value) { 35 | Boolean oldValue = get(); 36 | if (value == oldValue) { 37 | return oldValue; 38 | } 39 | super.set(value); 40 | if (value == Boolean.TRUE) { 41 | do { 42 | int old = $.getIntVolatile(this, index); 43 | int next = old + 1; 44 | if ($.compareAndSwapInt(this, index, old, next)) { 45 | return oldValue; 46 | } 47 | } while (true); 48 | } else { 49 | do { 50 | int old = $.getIntVolatile(this, index); 51 | int next = old - 1; 52 | if ($.compareAndSwapInt(this, index, old, next)) { 53 | return oldValue; 54 | } 55 | } while (true); 56 | } 57 | } 58 | 59 | @Override 60 | public Boolean get() { 61 | // Not volatile, this is good. If we miss another thread's update, that just saves time. 62 | if (count == 0) { 63 | return Boolean.FALSE; 64 | } 65 | return super.get(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/concurrent/threadlocal/CounterThreadLocalAssumeZero.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.concurrent.threadlocal; 2 | 3 | import sun.misc.Unsafe; 4 | 5 | import org.minimallycorrect.tickthreading.util.ReflectUtil; 6 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeAccess; 7 | 8 | /** 9 | * Accessing a field is much faster than a normal ThreadLocal get. Here, we maintain a field which counts the number of threads in which the variable is true In most cases, this allows us to avoid the performance hit of a hashmap lookup in ThreadLocal.get(), assuming the variable is normally false in all threads. 10 | */ 11 | public class CounterThreadLocalAssumeZero extends ThreadLocal { 12 | private static final Unsafe $ = UnsafeAccess.$; 13 | private static final long index = $.objectFieldOffset(ReflectUtil.getField(CounterThreadLocalAssumeZero.class, "count")); 14 | @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "CanBeFinal"}) 15 | private int count = 0; 16 | // Unsafe magics. 17 | 18 | @Override 19 | public Integer initialValue() { 20 | return 0; 21 | } 22 | 23 | @Override 24 | public Integer get() { 25 | throw new UnsupportedOperationException(); 26 | } 27 | 28 | public int getCount() { 29 | if (count == 0) { 30 | return 0; 31 | } 32 | return super.get(); 33 | } 34 | 35 | @Override 36 | public void set(Integer value) { 37 | throw new UnsupportedOperationException(); 38 | } 39 | 40 | public int increment() { 41 | int count = getCount(); 42 | int n = count + 1; 43 | super.set(n); 44 | if (count == 0) { 45 | do { 46 | int old = $.getIntVolatile(this, index); 47 | int next = old + 1; 48 | if ($.compareAndSwapInt(this, index, old, next)) { 49 | return 1; 50 | } 51 | } while (true); 52 | } 53 | return n; 54 | } 55 | 56 | public int decrement() { 57 | int count = getCount(); 58 | if (count == 0) { 59 | throw new Error("Can not decrement counter below 0."); 60 | } 61 | int n = count - 1; 62 | super.set(n); 63 | if (count == 1) { 64 | do { 65 | int old = $.getIntVolatile(this, index); 66 | int next = old - 1; 67 | if ($.compareAndSwapInt(this, index, old, next)) { 68 | return 0; 69 | } 70 | } while (true); 71 | } 72 | return n; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/config/Config.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.config; 2 | 3 | import java.io.*; 4 | import java.nio.file.*; 5 | 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | import lombok.val; 9 | 10 | import org.minimallycorrect.typedconfig.TypedConfig; 11 | import org.minimallycorrect.typedconfig.TypedConfig.Entry; 12 | 13 | @EqualsAndHashCode 14 | @ToString 15 | public class Config { 16 | public static final Config $ = loadConfig(); 17 | 18 | @Entry(description = "Target ticks per second") 19 | public int targetTps = 20; 20 | // TODO implement simple deadlock detector 21 | @Entry(description = "Maximum frozen time before deadlock detector assumes a deadlock has occurred") 22 | public int deadLockSeconds = 15; 23 | @Entry(description = "Enable world unloading") 24 | public boolean worldUnloading = true; 25 | @Entry(description = "Enable cleaning of unloaded worlds to aid in determining the cause of world leaks") 26 | public boolean worldCleaning = true; 27 | @Entry(description = "[extended] Separate per-world tick loops. When enabled each world's main loop is decoupled from other worlds. When disabled, the slowest world will reduce the TPS of all worlds.") 28 | public boolean separatePerWorldTickLoops = true; 29 | 30 | private static Config loadConfig() { 31 | val typedConfig = TypedConfig.of(Config.class, Paths.get("config", "tickthreading", "tickthreading.cfg")); 32 | val c = typedConfig.load(); 33 | typedConfig.save(c); 34 | return c; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/exception/ConcurrencyError.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.exception; 2 | 3 | public class ConcurrencyError extends Error { 4 | private static final long serialVersionUID = 0; 5 | 6 | public ConcurrencyError(String s) { 7 | super(s); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/exception/ThisIsNotAnError.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.exception; 2 | 3 | public class ThisIsNotAnError extends Error { 4 | private static final long serialVersionUID = 0; 5 | 6 | @Override 7 | public String getMessage() { 8 | return "This is not an error."; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/exception/ThreadStuckError.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.exception; 2 | 3 | /** 4 | * Thrown via Thread.stop() to try to resolve a deadlock, should be caught in Thread.run(), and thread should attempt to resume working. This is a bad idea according to good java developers, because it results in undefined behaviour. Which is correct, but in some cases we prefer undefined behaviour to definitely broken behaviour! 5 | */ 6 | public class ThreadStuckError extends Error { 7 | private static final long serialVersionUID = 0; 8 | private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; 9 | 10 | public ThreadStuckError() { 11 | super(); 12 | } 13 | 14 | public ThreadStuckError(final String message) { 15 | super(message); 16 | } 17 | 18 | public ThreadStuckError(final String message, final Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | public ThreadStuckError(final Throwable cause) { 23 | super(cause); 24 | } 25 | 26 | @Override 27 | public StackTraceElement[] getStackTrace() { 28 | return EMPTY_STACK_TRACE; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/log/Log.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.log; 2 | 3 | import java.nio.charset.*; 4 | import java.nio.file.*; 5 | import java.util.*; 6 | 7 | import lombok.SneakyThrows; 8 | import lombok.val; 9 | 10 | import org.apache.logging.log4j.Level; 11 | import org.apache.logging.log4j.LogManager; 12 | import org.apache.logging.log4j.Logger; 13 | import org.apache.logging.log4j.core.LogEvent; 14 | import org.apache.logging.log4j.core.LoggerContext; 15 | import org.apache.logging.log4j.core.appender.FileAppender; 16 | import org.apache.logging.log4j.core.config.Configuration; 17 | import org.apache.logging.log4j.core.layout.AbstractStringLayout; 18 | 19 | import org.minimallycorrect.tickthreading.util.PropertyUtil; 20 | import org.minimallycorrect.tickthreading.util.Version; 21 | 22 | import net.minecraft.server.MinecraftServer; 23 | import net.minecraft.tileentity.TileEntity; 24 | import net.minecraft.world.World; 25 | import net.minecraft.world.WorldServer; 26 | import net.minecraftforge.common.DimensionManager; 27 | import net.minecraftforge.fml.common.FMLCommonHandler; 28 | 29 | @SuppressWarnings({"UnusedDeclaration"}) 30 | public class Log { 31 | private static final Path logFolder = Paths.get("TickThreadingLogs"); 32 | private static final int numberOfLogFiles = PropertyUtil.get("numberOfLogFiles", 5); 33 | private static final String[] logsToSave = new String[]{ 34 | "@MOD_NAME@", 35 | "JavaPatcher", 36 | "JavaTransformer", 37 | "LibLoader", 38 | "Mixin", 39 | "ModPatcher", 40 | }; 41 | private static final Logger LOGGER = LogManager.getLogger(Version.NAME); 42 | 43 | static { 44 | createLogFiles(); 45 | } 46 | 47 | @SneakyThrows 48 | private static void createLogFiles() { 49 | if (!Files.isDirectory(logFolder)) 50 | Files.createDirectory(logFolder); 51 | for (int i = numberOfLogFiles; i >= 1; i--) { 52 | val currentFile = logFolder.resolve(Version.NAME + '.' + i + ".log"); 53 | if (Files.exists(currentFile)) { 54 | if (i == numberOfLogFiles) { 55 | Files.delete(currentFile); 56 | } else { 57 | Files.move(currentFile, logFolder.resolve(Version.NAME + '.' + (i + 1) + ".log")); 58 | } 59 | } 60 | } 61 | 62 | // TODO: rewrite this next bit using log4j public API at some point? gave up trying since it didn't work 63 | 64 | val saveFile = logFolder.resolve(Version.NAME + ".1.log"); 65 | final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); 66 | final Configuration config = ctx.getConfiguration(); 67 | // can't define a static field for an anonymous class, so no serialVersionUID 68 | @SuppressWarnings("serial") 69 | val layout = new AbstractStringLayout(Charset.forName("UTF-8")) { 70 | @Override 71 | public String toSerializable(LogEvent event) { 72 | return "[" + 73 | event.getLoggerName() + 74 | '/' + 75 | event.getThreadName() + 76 | '/' + 77 | event.getLevel().name() + 78 | "] " + 79 | event.getMessage().getFormattedMessage() + 80 | '\n'; 81 | } 82 | }; 83 | val appender = FileAppender.createAppender(saveFile.toAbsolutePath().toString(), "false", "false", "File", "true", "false", "false", "4000", layout, null, "false", null, config); 84 | appender.start(); 85 | config.addAppender(appender); 86 | for (val logName : logsToSave) 87 | ((org.apache.logging.log4j.core.Logger) LogManager.getLogger(logName)).addAppender(appender); 88 | ctx.updateLoggers(); 89 | } 90 | 91 | public static void error(String msg) { 92 | LOGGER.error(msg); 93 | } 94 | 95 | public static void warn(String msg) { 96 | LOGGER.warn(msg); 97 | } 98 | 99 | public static void info(String msg) { 100 | LOGGER.info(msg); 101 | } 102 | 103 | public static void trace(String msg) { 104 | LOGGER.trace(msg); 105 | } 106 | 107 | public static void error(String msg, Throwable t) { 108 | LOGGER.log(Level.ERROR, msg, t); 109 | } 110 | 111 | public static void warn(String msg, Throwable t) { 112 | LOGGER.log(Level.WARN, msg, t); 113 | } 114 | 115 | public static void info(String msg, Throwable t) { 116 | LOGGER.log(Level.INFO, msg, t); 117 | } 118 | 119 | public static void trace(String msg, Throwable t) { 120 | LOGGER.log(Level.TRACE, msg, t); 121 | } 122 | 123 | public static String name(World world) { 124 | if (world == null) { 125 | return "null world."; 126 | } else if (world.provider == null) { 127 | return "Broken world with ID ";// TODO + MinecraftServer.getServer().getId((WorldServer) world); 128 | } 129 | return world.getName(); 130 | } 131 | 132 | public static String classString(Object o) { 133 | return "c " + o.getClass().getName() + ' '; 134 | } 135 | 136 | public static String toString(Object o) { 137 | try { 138 | if (o instanceof World) { 139 | return name((World) o); 140 | } 141 | String cS = classString(o); 142 | String s = o.toString(); 143 | if (!s.startsWith(cS)) { 144 | s = cS + s; 145 | } 146 | if (o instanceof TileEntity) { 147 | TileEntity tE = (TileEntity) o; 148 | if (!s.contains(" x, y, z: ")) { 149 | s += tE.getPos().toString(); 150 | } 151 | } 152 | return s; 153 | } catch (Throwable t) { 154 | Log.error("Failed to perform toString on object of class " + o.getClass(), t); 155 | return "unknown"; 156 | } 157 | } 158 | 159 | public static void log(Level level, Throwable throwable, String s) { 160 | LOGGER.log(level, s, throwable); 161 | } 162 | 163 | public static String pos(final World world, final int x, final int z) { 164 | return "in " + world.getName() + ' ' + pos(x, z); 165 | } 166 | 167 | public static String pos(final int x, final int z) { 168 | return x + ", " + z; 169 | } 170 | 171 | public static String pos(final World world, final int x, final int y, final int z) { 172 | return "in " + world.getName() + ' ' + pos(x, y, z); 173 | } 174 | 175 | public static String pos(final int x, final int y, final int z) { 176 | return "at " + x + ", " + y + ", " + z; 177 | } 178 | 179 | private static String dumpWorld(World world) { 180 | boolean unloaded = world.unloaded; 181 | return (unloaded ? "un" : "") + "loaded world " + name(world) + '@' + System.identityHashCode(world) + ", dimension: " + world.getDimensionId(); 182 | } 183 | 184 | public static void checkWorlds() { 185 | MinecraftServer minecraftServer = FMLCommonHandler.instance().getMinecraftServerInstance(); 186 | val worlds = minecraftServer.worlds; 187 | int a = worlds.length; 188 | int b = DimensionManager.getWorlds().length; 189 | if (a != b) { 190 | Log.error("World counts mismatch.\n" + dumpWorlds()); 191 | } else if (hasDuplicates(worlds) || hasDuplicates(DimensionManager.getWorlds())) { 192 | Log.error("Duplicate worlds.\n" + dumpWorlds()); 193 | } else { 194 | for (WorldServer worldServer : worlds) { 195 | if (worldServer.unloaded || worldServer.provider == null) { 196 | Log.error("Broken/unloaded world in worlds list.\n" + dumpWorlds()); 197 | } 198 | } 199 | } 200 | } 201 | 202 | private static String dumpWorlds() { 203 | StringBuilder sb = new StringBuilder(); 204 | List dimensionManagerWorlds = new ArrayList<>(Arrays.asList(DimensionManager.getWorlds())); 205 | MinecraftServer minecraftServer = FMLCommonHandler.instance().getMinecraftServerInstance(); 206 | List minecraftServerWorlds = new ArrayList<>(Arrays.asList(minecraftServer.worlds)); 207 | sb.append("Worlds in dimensionManager: \n").append(dumpWorlds(dimensionManagerWorlds)); 208 | sb.append("Worlds in minecraftServer: \n").append(dumpWorlds(minecraftServerWorlds)); 209 | return sb.toString(); 210 | } 211 | 212 | private static boolean hasDuplicates(Object[] array) { 213 | return hasDuplicates(Arrays.asList(array)); 214 | } 215 | 216 | private static boolean hasDuplicates(List list) { 217 | if (list == null) { 218 | return false; 219 | } 220 | Set set = Collections.newSetFromMap(new IdentityHashMap<>()); 221 | for (Object o : list) { 222 | if (!set.add(o)) { 223 | return true; 224 | } 225 | } 226 | return false; 227 | } 228 | 229 | private static String dumpWorlds(final Collection worlds) { 230 | StringBuilder sb = new StringBuilder(); 231 | for (World world : worlds) { 232 | sb.append(dumpWorld(world)).append('\n'); 233 | } 234 | return sb.toString(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/log/LogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.log; 2 | 3 | import java.io.*; 4 | import java.text.*; 5 | import java.util.logging.*; 6 | 7 | import org.minimallycorrect.tickthreading.util.PropertyUtil; 8 | 9 | // TODO rework for log4j - not currently used 10 | public class LogFormatter extends Formatter { 11 | public static final String LINE_SEPARATOR = System.getProperty("line.separator"); 12 | protected static final boolean colorEnabled = PropertyUtil.get("logColor", false); 13 | private static final boolean simplifyMcLoggerName = PropertyUtil.get("logSimplifyMinecraftServer", true); 14 | private static final String endColor = "\033[39m"; 15 | private static SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss"); 16 | 17 | public static void setDateFormat(SimpleDateFormat dateFormat) { 18 | if (dateFormat != null) { 19 | LogFormatter.dateFormat = dateFormat; 20 | } 21 | } 22 | 23 | private static String getStartColor(Level level) { 24 | return "\033[" + getColorForLevel(level) + 'm'; 25 | } 26 | 27 | private static int getColorForLevel(Level level) { 28 | if (level == Level.SEVERE) { 29 | return 31; 30 | } else if (level == Level.WARNING) { 31 | return 33; 32 | } else if (level == Level.INFO) { 33 | return 32; 34 | } 35 | return 39; 36 | } 37 | 38 | @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") 39 | @Override 40 | public String format(LogRecord record) { 41 | StringBuilder formattedMessage = new StringBuilder(); 42 | long millis = record.getMillis(); 43 | String date; 44 | SimpleDateFormat dateFormat = LogFormatter.dateFormat; 45 | synchronized (dateFormat) { 46 | date = dateFormat.format(millis); 47 | } 48 | formattedMessage.append(date); 49 | Level level = record.getLevel(); 50 | formattedMessage.append(" ["); 51 | final boolean shouldColor = colorEnabled; 52 | if (shouldColor) { 53 | formattedMessage.append(getStartColor(level)); 54 | } 55 | formattedMessage.append(record.getLevel().getName().toUpperCase()); 56 | if (shouldColor) { 57 | formattedMessage.append(endColor); 58 | } 59 | formattedMessage.append("] "); 60 | 61 | String loggerName = record.getLoggerName(); 62 | String message = record.getMessage(); 63 | if (simplifyMcLoggerName && loggerName.equals("Minecraft-Server")) { 64 | loggerName = "Minecraft"; 65 | } else if (message.startsWith("[") && message.contains("]")) { 66 | loggerName = null; 67 | } else if (loggerName.contains(".")) { 68 | loggerName = loggerName.substring(loggerName.lastIndexOf('.') + 1); 69 | } 70 | if (loggerName != null) { 71 | formattedMessage.append('[').append(loggerName).append("] "); 72 | } 73 | 74 | formattedMessage.append(message).append(LINE_SEPARATOR); 75 | 76 | // No need to throw this, we're in a log formatter! 77 | @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 78 | Throwable throwable = record.getThrown(); 79 | if (throwable != null) { 80 | if (throwable.getClass().getName().endsWith("ThreadStuckError")) { 81 | return ""; 82 | } 83 | if (throwable.getStackTrace().length == 0) { 84 | formattedMessage.append("Stack trace unavailable for ").append(String.valueOf(throwable)).append('-').append(throwable.getClass().getName()).append(". Add -XX:-OmitStackTraceInFastThrow to your java parameters to see all stack traces.").append(LINE_SEPARATOR); 85 | } else { 86 | StringWriter stackTraceWriter = new StringWriter(); 87 | // No need to close this - StringWriter.close() does nothing, and PrintWriter.close() just calls it. 88 | //noinspection IOResourceOpenedButNotSafelyClosed 89 | throwable.printStackTrace(new PrintWriter(stackTraceWriter)); 90 | String output = stackTraceWriter.toString(); 91 | if (throwable.getClass() == Throwable.class) { 92 | output = output.replace("java.lang.Throwable\n", ""); 93 | } 94 | 95 | formattedMessage.append(output); 96 | } 97 | 98 | if (throwable.getCause() != null && throwable.getCause().getStackTrace().length == 0) { 99 | formattedMessage.append("Stack trace unavailable for cause: ").append(String.valueOf(throwable.getCause())).append('-').append(throwable.getCause().getClass().getName()).append(". Add -XX:-OmitStackTraceInFastThrow to your java parameters to see all stack traces.").append(LINE_SEPARATOR); 100 | } 101 | } 102 | 103 | return formattedMessage.toString(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/forge/MixinDimensionManager.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.extended.forge; 2 | 3 | import java.util.Hashtable; 4 | 5 | import org.minimallycorrect.javatransformer.api.code.RETURN; 6 | import org.minimallycorrect.mixin.*; 7 | import org.minimallycorrect.tickthreading.config.Config; 8 | import org.minimallycorrect.tickthreading.reporting.LeakDetector; 9 | 10 | import net.minecraft.world.WorldServer; 11 | import net.minecraftforge.common.DimensionManager; 12 | 13 | @Mixin 14 | public abstract class MixinDimensionManager extends DimensionManager { 15 | @Injectable 16 | public static void scheduleLeakCheck(WorldServer w) { 17 | LeakDetector.scheduleLeakCheck(w, w.getName()); 18 | } 19 | 20 | @Injectable 21 | public static void abortWorldUnloading() { 22 | if (unloadQueue.isEmpty()) { 23 | throw RETURN.VOID(); 24 | } 25 | if (!Config.$.worldUnloading) { 26 | unloadQueue.clear(); 27 | throw RETURN.VOID(); 28 | } 29 | } 30 | 31 | @Inject(injectable = "abortWorldUnloading", type = Type.BODY) 32 | @Inject(injectable = "scheduleLeakCheck", type = Type.METHOD_CALL, value = "flush") 33 | public static void unloadWorlds(@SuppressWarnings({"UseOfObsoleteCollectionType", "unused"}) Hashtable worldTickTimes) {} 34 | 35 | @Overwrite 36 | public static void initDimension(int dim) { 37 | // TODO: re-implement this safely 38 | throw new UnsupportedOperationException(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/forge/MixinOreDictionary.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.extended.forge; 2 | 3 | import java.util.List; 4 | 5 | import lombok.val; 6 | 7 | import org.minimallycorrect.mixin.Mixin; 8 | import org.minimallycorrect.mixin.Overwrite; 9 | 10 | import it.unimi.dsi.fastutil.ints.IntOpenHashSet; 11 | import net.minecraft.item.Item; 12 | import net.minecraft.item.ItemStack; 13 | import net.minecraftforge.fml.common.FMLLog; 14 | import net.minecraftforge.oredict.OreDictionary; 15 | 16 | @Mixin 17 | public abstract class MixinOreDictionary extends OreDictionary { 18 | @Overwrite 19 | public static int[] getOreIDs(ItemStack stack) { 20 | if (stack == null) 21 | throw new IllegalArgumentException(); 22 | 23 | val item = stack.getItem(); 24 | if (item == null) 25 | throw new IllegalArgumentException(); 26 | 27 | val delegate = item.delegate; 28 | val registryName = delegate.name(); 29 | if (registryName == null) { 30 | FMLLog.log.debug("Attempted to find the oreIDs for an unregistered object (%s). This won't work very well.", stack); 31 | return new int[0]; 32 | } 33 | 34 | // TODO: cache this? 35 | 36 | val set = new IntOpenHashSet(); 37 | int id = Item.REGISTRY.getIDForObject(delegate.get()); 38 | List ids = stackToId.get(id); 39 | if (ids != null) { 40 | set.addAll(ids); 41 | } 42 | 43 | ids = stackToId.get(id | stack.getItemDamage() + 1 << 16); 44 | if (ids != null) { 45 | set.addAll(ids); 46 | } 47 | 48 | val ret = new int[set.size()]; 49 | val ie = set.iterator(); 50 | int i = 0; 51 | while (ie.hasNext()) 52 | ret[i++] = ie.nextInt(); 53 | 54 | return ret; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/package-info.java: -------------------------------------------------------------------------------- 1 | @Mixin 2 | package org.minimallycorrect.tickthreading.mixin.extended; 3 | 4 | import org.minimallycorrect.mixin.Mixin; 5 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/server/MixinMinecraftServer.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.extended.server; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.FutureTask; 8 | 9 | import lombok.SneakyThrows; 10 | import lombok.val; 11 | 12 | import com.google.common.util.concurrent.Futures; 13 | import com.google.common.util.concurrent.ListenableFuture; 14 | import com.google.common.util.concurrent.ListenableFutureTask; 15 | import com.mojang.authlib.GameProfile; 16 | 17 | import org.minimallycorrect.mixin.*; 18 | import org.minimallycorrect.modpatcher.api.UsedByPatch; 19 | import org.minimallycorrect.tickthreading.config.Config; 20 | import org.minimallycorrect.tickthreading.log.Log; 21 | 22 | import net.minecraft.network.ServerStatusResponse; 23 | import net.minecraft.server.MinecraftServer; 24 | import net.minecraft.util.Util; 25 | import net.minecraft.util.math.MathHelper; 26 | import net.minecraftforge.fml.common.FMLCommonHandler; 27 | 28 | @Mixin 29 | public abstract class MixinMinecraftServer extends MinecraftServer { 30 | @Add 31 | private static final Object worldWait_ = new Object(); 32 | 33 | @Injectable 34 | private static void mainLoopCaller() { 35 | 36 | } 37 | 38 | @Overwrite 39 | public ListenableFuture callFromMainThread(Callable callable) { 40 | if (Config.$.separatePerWorldTickLoops) 41 | throw new UnsupportedOperationException("Missing required patch. TickThreading should patch this call to use per-world task"); 42 | 43 | if (!this.isCallingFromMinecraftThread() && !this.isServerStopped()) { 44 | val futureTask = ListenableFutureTask.create(callable); 45 | synchronized (this.futureTaskQueue) { 46 | this.futureTaskQueue.add(futureTask); 47 | return futureTask; 48 | } 49 | } 50 | try { 51 | return Futures.immediateFuture(callable.call()); 52 | } catch (Exception exception) { 53 | return Futures.immediateFailedCheckedFuture(exception); 54 | } 55 | } 56 | 57 | @Overwrite 58 | public ListenableFuture addScheduledTask(Runnable runnableToSchedule) { 59 | return callFromMainThread(Executors.callable(runnableToSchedule)); 60 | } 61 | 62 | @Add 63 | public void waitForWorldTick() { 64 | val worldWait = MinecraftServer.worldWait; 65 | synchronized (worldWait) { 66 | worldWait.notify(); 67 | } 68 | } 69 | 70 | @Matcher 71 | private void matchApplyServerIconToResponseMatcher() { 72 | //noinspection ConstantConditions 73 | this.applyServerIconToResponse(null); 74 | } 75 | 76 | @Override 77 | @Inject(injectable = "mainLoopCaller", position = Position.AFTER, type = Type.METHOD_CALL, match = "matchApplyServerIconToResponseMatcher") 78 | public abstract void run(); 79 | 80 | @SneakyThrows 81 | @UsedByPatch("minecraft-extended.xml") 82 | @Add 83 | protected void mainLoop() { 84 | long TARGET_TPS = Config.$.targetTps; 85 | long TARGET_TICK_TIME = 1000000000 / TARGET_TPS; 86 | long startTime = System.nanoTime(); 87 | float tickTime = 1; 88 | this.serverIsRunning = true; 89 | while (this.serverRunning) { 90 | ++tickCounter; 91 | try { 92 | this.tick(startTime); 93 | } catch (Exception e) { 94 | Log.error("Exception in main tick loop", e); 95 | } 96 | long endTime = System.nanoTime(); 97 | this.tickTimeArray[this.tickCounter % 100] = endTime - startTime; 98 | 99 | currentTime = endTime; 100 | tickTime = tickTime * 0.98f + ((endTime - startTime) * 0.02f); 101 | 102 | long wait = (TARGET_TICK_TIME - (endTime - startTime)) / 1000000; 103 | if (wait > 0 && TARGET_TICK_TIME * TARGET_TPS / tickTime > TARGET_TPS) { 104 | Thread.sleep(wait); 105 | startTime = System.currentTimeMillis(); 106 | } else { 107 | startTime = endTime; 108 | } 109 | } 110 | } 111 | 112 | @Overwrite 113 | public void tick() { 114 | throw new UnsupportedOperationException(); 115 | } 116 | 117 | @Add 118 | public void tick(long startTime) { 119 | FMLCommonHandler.instance().onPreServerTick(); 120 | 121 | val separateTicks = Config.$.separatePerWorldTickLoops; 122 | if (!separateTicks) { 123 | synchronized (this.futureTaskQueue) { 124 | FutureTask c; 125 | while ((c = this.futureTaskQueue.poll()) != null) 126 | Util.runTask(c, LOGGER); 127 | } 128 | } 129 | 130 | net.minecraftforge.common.chunkio.ChunkIOExecutor.tick(); 131 | 132 | if (!separateTicks) { 133 | synchronized (worldWait) { 134 | worldWait.notifyAll(); 135 | } 136 | } 137 | 138 | net.minecraftforge.common.DimensionManager.unloadWorlds(worldTickTimes); 139 | this.getNetworkSystem().networkTick(); 140 | this.playerList.onTick(); 141 | this.getFunctionManager().update(); 142 | for (val tickable : this.tickables) 143 | tickable.update(); 144 | 145 | if (startTime - this.nanoTimeSinceStatusRefresh >= 5000000000L) { 146 | this.nanoTimeSinceStatusRefresh = startTime; 147 | this.statusResponse.setPlayers(new ServerStatusResponse.Players(this.getMaxPlayers(), this.getCurrentPlayerCount())); 148 | GameProfile[] agameprofile = new GameProfile[Math.min(this.getCurrentPlayerCount(), 12)]; 149 | int j = MathHelper.getInt(this.random, 0, this.getCurrentPlayerCount() - agameprofile.length); 150 | 151 | for (int k = 0; k < agameprofile.length; ++k) { 152 | agameprofile[k] = this.playerList.getPlayers().get(j + k).getGameProfile(); 153 | } 154 | 155 | Collections.shuffle(Arrays.asList(agameprofile)); 156 | this.statusResponse.getPlayers().setPlayers(agameprofile); 157 | this.statusResponse.invalidateJson(); 158 | } 159 | 160 | if (this.tickCounter % 900 == 0) { 161 | this.playerList.saveAllPlayerData(); 162 | // TODO: iirc this ends up forcibly saving all dirty chunks immediately every 900 secs -> lag spike? 163 | // definitely needs to go from here due to per-world threading 164 | // but probably not having it here is a problem 165 | this.saveAllWorlds(true); 166 | } 167 | 168 | if (!this.usageSnooper.isSnooperRunning() && this.tickCounter > 100) { 169 | this.usageSnooper.startSnooper(); 170 | } 171 | 172 | if (this.tickCounter % 6000 == 0) { 173 | this.usageSnooper.addMemoryStatsToSnooper(); 174 | } 175 | 176 | FMLCommonHandler.instance().onPostServerTick(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/world/MixinWorld.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.extended.world; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.*; 5 | 6 | import lombok.val; 7 | 8 | import com.google.common.util.concurrent.Futures; 9 | import com.google.common.util.concurrent.ListenableFuture; 10 | import com.google.common.util.concurrent.ListenableFutureTask; 11 | 12 | import org.minimallycorrect.mixin.Add; 13 | import org.minimallycorrect.mixin.Mixin; 14 | 15 | import net.minecraft.world.World; 16 | 17 | @Mixin 18 | public abstract class MixinWorld extends World { 19 | @Add 20 | private final ArrayDeque tasks_ = new ArrayDeque<>(); 21 | @Add 22 | private Thread worldThread_; 23 | 24 | @Add 25 | public ListenableFuture callFromMainThread(Callable callable) { 26 | if (!unloaded && Thread.currentThread() == worldThread) { 27 | val task = ListenableFutureTask.create(callable); 28 | tasks.add(task); 29 | return task; 30 | } 31 | try { 32 | return Futures.immediateFuture(callable.call()); 33 | } catch (Exception exception) { 34 | return Futures.immediateFailedCheckedFuture(exception); 35 | } 36 | } 37 | 38 | @Add 39 | public ListenableFuture addScheduledTask(Runnable runnableToSchedule) { 40 | return this. callFromMainThread(Executors.callable(runnableToSchedule)); 41 | } 42 | 43 | @Add 44 | public void runTasks() { 45 | val tasks = this.tasks; 46 | Runnable task; 47 | while ((task = tasks.poll()) != null) 48 | task.run(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/extended/world/MixinWorldServer.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.extended.world; 2 | 3 | import lombok.val; 4 | 5 | import org.minimallycorrect.mixin.Add; 6 | import org.minimallycorrect.mixin.Mixin; 7 | 8 | import net.minecraft.crash.CrashReport; 9 | import net.minecraft.network.play.server.SPacketTimeUpdate; 10 | import net.minecraft.util.ReportedException; 11 | import net.minecraft.world.WorldServer; 12 | import net.minecraftforge.fml.common.FMLCommonHandler; 13 | 14 | @Mixin 15 | public abstract class MixinWorldServer extends WorldServer { 16 | @Add 17 | public void tickWithEvents() { 18 | if (this.getTotalWorldTime() % 20 == 0) { 19 | mcServer.getPlayerList().sendPacketToAllPlayersInDimension(new SPacketTimeUpdate(getTotalWorldTime(), getWorldTime(), getGameRules().getBoolean("doDaylightCycle")), provider.getDimension()); 20 | } 21 | FMLCommonHandler.instance().onPreWorldTick(this); 22 | try { 23 | tick(); 24 | } catch (Throwable throwable) { 25 | val crashreport = CrashReport.makeCrashReport(throwable, "Exception ticking world"); 26 | addWorldInfoToCrashReport(crashreport); 27 | throw new ReportedException(crashreport); 28 | } 29 | try { 30 | updateEntities(); 31 | } catch (Throwable throwable) { 32 | val crashreport = CrashReport.makeCrashReport(throwable, "Exception ticking world entities"); 33 | addWorldInfoToCrashReport(crashreport); 34 | throw new ReportedException(crashreport); 35 | } 36 | FMLCommonHandler.instance().onPostWorldTick(this); 37 | getEntityTracker().tick(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/forge/MixinFMLCommonHandler.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.forge; 2 | 3 | import org.minimallycorrect.mixin.Mixin; 4 | import org.minimallycorrect.mixin.Overwrite; 5 | 6 | import net.minecraftforge.fml.common.FMLCommonHandler; 7 | import net.minecraftforge.fml.relauncher.Side; 8 | 9 | @Mixin 10 | public abstract class MixinFMLCommonHandler extends FMLCommonHandler { 11 | @Overwrite 12 | public Side getEffectiveSide() { 13 | return Side.SERVER; 14 | } 15 | 16 | @Overwrite 17 | public Side getSide() { 18 | return Side.SERVER; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/package-info.java: -------------------------------------------------------------------------------- 1 | @Mixin 2 | package org.minimallycorrect.tickthreading.mixin; 3 | 4 | import org.minimallycorrect.mixin.Mixin; 5 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/world/MixinChunk.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.world; 2 | 3 | import org.minimallycorrect.mixin.Mixin; 4 | import org.minimallycorrect.mixin.Overwrite; 5 | 6 | import net.minecraft.world.chunk.Chunk; 7 | 8 | @Mixin 9 | public abstract class MixinChunk extends Chunk { 10 | @Overwrite 11 | public boolean needsSaving(boolean force) { 12 | if (force) { 13 | return dirty || (hasEntities && this.world.getTotalWorldTime() != this.lastSaveTime); 14 | } 15 | 16 | return (dirty || hasEntities) && this.world.getTotalWorldTime() >= this.lastSaveTime + 2111L; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/world/MixinChunkProviderServer.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.world; 2 | 3 | import org.minimallycorrect.mixin.Mixin; 4 | import org.minimallycorrect.mixin.Overwrite; 5 | 6 | import net.minecraft.world.chunk.Chunk; 7 | import net.minecraft.world.gen.ChunkProviderServer; 8 | 9 | @Mixin 10 | public abstract class MixinChunkProviderServer extends ChunkProviderServer { 11 | @Overwrite 12 | public boolean saveChunks(boolean force) { 13 | int i = 0; 14 | 15 | // id2ChunkMap should not be modified while we're doing this - don't duplicate it to a list like original MC code 16 | for (Chunk chunk : this.id2ChunkMap.values()) { 17 | if (force) { 18 | this.saveChunkExtraData(chunk); 19 | } 20 | 21 | if (chunk.needsSaving(force)) { 22 | this.saveChunkData(chunk); 23 | chunk.setModified(false); 24 | if (!force && ++i == 24) { 25 | return false; 26 | } 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/world/MixinWorld.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.world; 2 | 3 | import org.minimallycorrect.mixin.Add; 4 | import org.minimallycorrect.mixin.Mixin; 5 | 6 | import net.minecraft.world.World; 7 | 8 | @Mixin 9 | public abstract class MixinWorld extends World { 10 | @Add 11 | public boolean unloaded_; 12 | @Add 13 | private String cachedName_; 14 | 15 | @Add 16 | public int getDimensionId() { 17 | return provider.getDimensionType().getId(); 18 | } 19 | 20 | @Add 21 | public String getName() { 22 | String name = cachedName; 23 | if (name != null) { 24 | return name; 25 | } 26 | int dimensionId = getDimensionId(); 27 | name = worldInfo.getWorldName(); 28 | if (name.equals("DIM" + dimensionId) || "world".equals(name)) { 29 | name = provider.getDimensionType().getName(); 30 | } 31 | if (name.startsWith("world_") && name.length() != 6) { 32 | name = name.substring(6); 33 | } 34 | name += '/' + dimensionId + (isRemote ? "-r" : ""); 35 | cachedName = name; 36 | return name; 37 | } 38 | 39 | @Add 40 | public String toString() { 41 | return "World " + System.identityHashCode(this) + " " + getName(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mixin/world/MixinWorldEntitySpawner.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mixin.world; 2 | 3 | import java.util.Collection; 4 | import java.util.EnumMap; 5 | 6 | import lombok.val; 7 | 8 | import org.minimallycorrect.mixin.Add; 9 | import org.minimallycorrect.mixin.Mixin; 10 | import org.minimallycorrect.mixin.Overwrite; 11 | import org.minimallycorrect.tickthreading.collection.LongList; 12 | import org.minimallycorrect.tickthreading.collection.LongSet; 13 | 14 | import net.minecraft.block.state.IBlockState; 15 | import net.minecraft.entity.EntityLiving; 16 | import net.minecraft.entity.EnumCreatureType; 17 | import net.minecraft.entity.IEntityLivingData; 18 | import net.minecraft.entity.player.EntityPlayer; 19 | import net.minecraft.init.Blocks; 20 | import net.minecraft.util.EnumFacing; 21 | import net.minecraft.util.math.BlockPos; 22 | import net.minecraft.world.World; 23 | import net.minecraft.world.WorldEntitySpawner; 24 | import net.minecraft.world.WorldProviderHell; 25 | import net.minecraft.world.WorldServer; 26 | import net.minecraft.world.biome.Biome; 27 | import net.minecraft.world.chunk.Chunk; 28 | import net.minecraftforge.event.ForgeEventFactory; 29 | import net.minecraftforge.fml.common.eventhandler.Event; 30 | 31 | @Mixin 32 | public abstract class MixinWorldEntitySpawner extends WorldEntitySpawner { 33 | private static final int closeRange = 1; 34 | private static final int farRange = 5; 35 | private static final int spawnVariance = 6; 36 | private static final int triesPerCreatureType = 4; 37 | private static final int clumping = 7; 38 | private static final int mobClumping = 5; 39 | private static final int maxChunksPerPlayer = (farRange * farRange * 4) - (closeRange * closeRange * 4); 40 | 41 | @Add 42 | private static long hash(int x, int z) { 43 | return (((long) x) << 32) | (z & 0xffffffffL); 44 | } 45 | 46 | @Add 47 | private static Chunk getChunkFromBlockCoords(WorldServer w, int x, int z) { 48 | return w.getChunkProvider().getLoadedChunk(x >> 4, z >> 4); 49 | } 50 | 51 | @Add 52 | private static int getPseudoRandomHeightValue(int wX, int wZ, WorldServer worldServer, boolean surface, int gapChance) { 53 | Chunk chunk = getChunkFromBlockCoords(worldServer, wX, wZ); 54 | if (chunk == null) { 55 | return -1; 56 | } 57 | int x = wX & 15; 58 | int z = wZ & 15; 59 | int height = chunk.getHeightValue(x, z); 60 | if (surface) 61 | return height; 62 | int maxHeight = worldServer.provider.getActualHeight(); 63 | if (height >= maxHeight) { 64 | height = -1; 65 | } 66 | boolean inGap = false; 67 | int lastGap = 0; 68 | val blockPos = new BlockPos.MutableBlockPos(wX, 0, wZ); 69 | for (int y = 1; y < height; y++) { 70 | val block = chunk.getBlockState(x, y, z); 71 | blockPos.setY(y); 72 | if (block.getBlock() == Blocks.AIR || !block.isSideSolid(worldServer, blockPos, EnumFacing.UP)) { 73 | if (!inGap) { 74 | inGap = true; 75 | if (gapChance++ % 3 == 0) { 76 | return y; 77 | } 78 | lastGap = y; 79 | } 80 | } else { 81 | inGap = false; 82 | } 83 | } 84 | return lastGap == 0 ? height : lastGap; 85 | } 86 | 87 | @Overwrite 88 | public int findChunksForSpawning(WorldServer worldServer, boolean peaceful, boolean hostile, boolean animal) { 89 | if (worldServer.getWorldTime() % clumping != 0) { 90 | return 0; 91 | } 92 | float entityMultiplier = worldServer.playerEntities.size() * 1.5f; 93 | if (entityMultiplier == 0) { 94 | return 0; 95 | } 96 | 97 | val profiler = worldServer.profiler; 98 | profiler.startSection("creatureTypes"); 99 | val p = worldServer.getChunkProvider(); 100 | val hell = worldServer.provider instanceof WorldProviderHell; 101 | boolean dayTime = !hell && worldServer.isDaytime(); 102 | float mobMultiplier = entityMultiplier * (dayTime ? 1 : 2); 103 | val requiredSpawns = new EnumMap(EnumCreatureType.class); 104 | for (EnumCreatureType creatureType : EnumCreatureType.values()) { 105 | if (hell && creatureType == EnumCreatureType.WATER_CREATURE) 106 | continue; 107 | 108 | int count = (int) ((creatureType.getPeacefulCreature() ? entityMultiplier : mobMultiplier) * creatureType.getMaxNumberOfCreature()); 109 | if (creatureType.getPeacefulCreature() && !peaceful || creatureType.getAnimal() && !animal || !creatureType.getPeacefulCreature() && !hostile) 110 | continue; 111 | 112 | val current = worldServer.countEntities(creatureType, true); 113 | if (count > current) 114 | requiredSpawns.put(creatureType, count - current); 115 | } 116 | profiler.endSection(); 117 | 118 | if (requiredSpawns.isEmpty()) { 119 | return 0; 120 | } 121 | 122 | profiler.startSection("spawnableChunks"); 123 | int attemptedSpawnedMobs = 0; 124 | val closeChunks = new LongSet(); 125 | Collection entityPlayers = worldServer.playerEntities; 126 | LongList spawnableChunks = new LongList(entityPlayers.size() * maxChunksPerPlayer); 127 | for (EntityPlayer entityPlayer : entityPlayers) { 128 | int pX = entityPlayer.chunkCoordX; 129 | int pZ = entityPlayer.chunkCoordZ; 130 | int x = pX - closeRange; 131 | int maxX = pX + closeRange; 132 | int startZ = pZ - closeRange; 133 | int maxZ = pZ + closeRange; 134 | for (; x <= maxX; x++) { 135 | for (int z = startZ; z <= maxZ; z++) { 136 | closeChunks.add(hash(x, z)); 137 | } 138 | } 139 | } 140 | for (EntityPlayer entityPlayer : entityPlayers) { 141 | int pX = entityPlayer.chunkCoordX; 142 | int pZ = entityPlayer.chunkCoordZ; 143 | int x = pX - farRange; 144 | int maxX = pX + farRange; 145 | int startZ = pZ - farRange; 146 | int maxZ = pZ + farRange; 147 | for (; x <= maxX; x++) { 148 | for (int z = startZ; z <= maxZ; z++) { 149 | long hash = hash(x, z); 150 | if (!closeChunks.contains(hash) || !p.chunkExists(x, z)) { 151 | spawnableChunks.add(hash); 152 | } 153 | } 154 | } 155 | } 156 | profiler.endSection(); 157 | 158 | int size = spawnableChunks.size; 159 | if (size == 0) 160 | return 0; 161 | 162 | profiler.startSection("spawnMobs"); 163 | 164 | int surfaceChance = 0; 165 | int gapChance = 0; 166 | SpawnLoop: for (val entry : requiredSpawns.entrySet()) { 167 | val creatureType = entry.getKey(); 168 | val tries = Math.max(triesPerCreatureType, entry.getValue() / (triesPerCreatureType * mobClumping)); 169 | for (int j = 0; j < tries; j++) { 170 | long hash = spawnableChunks.get(worldServer.rand.nextInt(size)); 171 | int x = (int) (hash >> 32); 172 | int z = (int) hash; 173 | val chunk = p.getLoadedChunk(x, z); 174 | if (chunk == null) 175 | continue; 176 | int sX = x << 4 + worldServer.rand.nextInt(16); 177 | int sZ = z << 4 + worldServer.rand.nextInt(16); 178 | boolean surface = !hell && (creatureType.getPeacefulCreature() || (dayTime ? surfaceChance++ % 5 == 0 : surfaceChance++ % 5 != 0)); 179 | int gap = gapChance++; 180 | int sY; 181 | if (creatureType == EnumCreatureType.WATER_CREATURE) { 182 | String biomeName = chunk.getBiome(new BlockPos(sX, 64, sZ), worldServer.provider.getBiomeProvider()).getBiomeName(); 183 | if (!"Ocean".equals(biomeName) && !"River".equals(biomeName)) { 184 | continue; 185 | } 186 | sY = getPseudoRandomHeightValue(sX, sZ, worldServer, true, gap) - 2; 187 | } else { 188 | sY = getPseudoRandomHeightValue(sX, sZ, worldServer, surface, gap); 189 | } 190 | if (sY <= 0) { 191 | continue; 192 | } 193 | // TODO: Fix this check? 194 | if (true) { 195 | //if (chunk.getBlockState(sX, sY, sZ).getMaterial() == creatureType.getCreatureMaterial()) { 196 | IEntityLivingData unusedIEntityLivingData = null; 197 | for (int i = 0; i < mobClumping; i++) { 198 | int ssX = sX + (worldServer.rand.nextInt(spawnVariance) - spawnVariance / 2); 199 | int ssZ = sZ + (worldServer.rand.nextInt(spawnVariance) - spawnVariance / 2); 200 | 201 | if (!p.chunkExists(ssX >> 4, ssZ >> 4)) 202 | continue; 203 | 204 | int ssY; 205 | IBlockState state = null; 206 | if (creatureType == EnumCreatureType.WATER_CREATURE) { 207 | ssY = sY; 208 | } else if (creatureType == EnumCreatureType.AMBIENT) { 209 | ssY = worldServer.rand.nextInt(63) + 1; 210 | } else { 211 | ssY = getPseudoRandomHeightValue(ssX, ssZ, worldServer, surface, gap); 212 | if (ssY <= 0) 213 | continue; 214 | state = chunk.getBlockState(ssX, ssY - 1, ssZ); 215 | if (!state.getBlock().canCreatureSpawn(state, worldServer, new BlockPos(ssX, ssY - 1, ssZ), null)) 216 | continue; 217 | } 218 | 219 | if (creatureType != EnumCreatureType.WATER_CREATURE) { 220 | if (state == null) 221 | state = chunk.getBlockState(ssX, ssY - 1, ssZ); 222 | if (state.getMaterial().isLiquid()) 223 | continue; 224 | } 225 | 226 | Biome.SpawnListEntry creatureClass = worldServer.getSpawnListEntryForTypeAt(creatureType, new BlockPos(ssX, ssY, ssZ)); 227 | if (creatureClass == null) 228 | break; 229 | 230 | EntityLiving spawnedEntity; 231 | try { 232 | spawnedEntity = creatureClass.entityClass.getConstructor(World.class).newInstance(worldServer); 233 | spawnedEntity.setLocationAndAngles((double) ssX, (double) ssY, (double) ssZ, worldServer.rand.nextFloat() * 360.0F, 0.0F); 234 | 235 | val canSpawn = ForgeEventFactory.canEntitySpawn(spawnedEntity, worldServer, ssX, ssY, ssZ, null); 236 | if (canSpawn == Event.Result.ALLOW || (canSpawn == Event.Result.DEFAULT && spawnedEntity.getCanSpawnHere())) { 237 | worldServer.spawnEntity(spawnedEntity); 238 | if (!ForgeEventFactory.doSpecialSpawn(spawnedEntity, worldServer, ssX, ssY, ssZ, null)) { 239 | unusedIEntityLivingData = spawnedEntity.onInitialSpawn(worldServer.getDifficultyForLocation(new BlockPos(spawnedEntity)), unusedIEntityLivingData); 240 | } 241 | } 242 | attemptedSpawnedMobs++; 243 | } catch (Exception e) { 244 | System.err.println("Failed to spawn entity " + creatureClass); 245 | e.printStackTrace(); 246 | break SpawnLoop; 247 | } 248 | } 249 | } 250 | if (attemptedSpawnedMobs >= size) 251 | break SpawnLoop; 252 | } 253 | } 254 | profiler.endSection(); 255 | return attemptedSpawnedMobs; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mod/TickThreading.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mod; 2 | 3 | import lombok.val; 4 | 5 | import sun.misc.Signal; 6 | import sun.misc.SignalHandler; 7 | 8 | import org.minimallycorrect.tickthreading.log.Log; 9 | 10 | import net.minecraftforge.fml.common.FMLCommonHandler; 11 | import net.minecraftforge.fml.common.Mod; 12 | import net.minecraftforge.fml.common.event.FMLInitializationEvent; 13 | 14 | @Mod(modid = "@MOD_ID@", version = "@MOD_VERSION@", name = "@MOD_NAME@", acceptableRemoteVersions = "*", acceptedMinecraftVersions = "[@MC_VERSION@]") 15 | public class TickThreading { 16 | private static void handleSignal(String name, SignalHandler handler) { 17 | try { 18 | Signal.handle(new Signal(name), handler); 19 | } catch (IllegalArgumentException ignored) {} 20 | } 21 | 22 | @Mod.EventHandler 23 | public void init(FMLInitializationEvent event) { 24 | SignalHandler handler = signal -> { 25 | Log.info("Received signal " + signal.getName() + ". Stopping server."); 26 | val server = FMLCommonHandler.instance().getMinecraftServerInstance(); 27 | server.initiateShutdown(); 28 | while (!server.isServerStopped()) { 29 | try { 30 | Thread.sleep(100); 31 | } catch (InterruptedException ignored) {} 32 | } 33 | }; 34 | handleSignal("TERM", handler); 35 | handleSignal("INT", handler); 36 | handleSignal("HUP", handler); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/mod/TickThreadingCore.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.mod; 2 | 3 | import java.util.Map; 4 | 5 | import org.minimallycorrect.libloader.LibLoader; 6 | import org.minimallycorrect.modpatcher.api.ModPatcher; 7 | import org.minimallycorrect.tickthreading.config.Config; 8 | import org.minimallycorrect.tickthreading.log.Log; 9 | import org.minimallycorrect.tickthreading.util.PropertyUtil; 10 | import org.minimallycorrect.tickthreading.util.Version; 11 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeUtil; 12 | 13 | import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; 14 | 15 | @IFMLLoadingPlugin.Name("@MOD_NAME@Core") 16 | @IFMLLoadingPlugin.SortingIndex(1002) 17 | public class TickThreadingCore implements IFMLLoadingPlugin { 18 | //noinspection ResultOfMethodCallIgnored 19 | static { 20 | Log.classString(""); 21 | LibLoader.init(); 22 | //Load config file early so invalid config crashes fast, not 2 minutes into loading a large modpack 23 | Config.$.getClass(); 24 | 25 | // By default, disable FML security manager. 26 | // All it does currently is stop System.exit() calls 27 | // It adds some (small) overhead to many operations and we don't really need it 28 | if (PropertyUtil.get("removeSecurityManager", true)) 29 | UnsafeUtil.removeSecurityManager(); 30 | 31 | Log.info(Version.DESCRIPTION + " CoreMod initialised"); 32 | } 33 | 34 | @Override 35 | public String[] getASMTransformerClass() { 36 | return new String[0]; 37 | } 38 | 39 | @Override 40 | public String getModContainerClass() { 41 | return null; 42 | } 43 | 44 | @Override 45 | public String getSetupClass() { 46 | return ModPatcher.getSetupClass(); 47 | } 48 | 49 | @Override 50 | public void injectData(Map map) { 51 | ModPatcher.loadMixins("org.minimallycorrect.tickthreading.mixin"); 52 | ModPatcher.loadPatches(TickThreadingCore.class.getResourceAsStream("/patches/minecraft.xml")); 53 | if (Version.EXTENDED) 54 | ModPatcher.loadPatches(TickThreadingCore.class.getResourceAsStream("/patches/minecraft-extended.xml")); 55 | } 56 | 57 | @Override 58 | public String getAccessTransformerClass() { 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/reporting/LeakDetector.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.reporting; 2 | 3 | import java.lang.ref.*; 4 | import java.util.*; 5 | import java.util.concurrent.*; 6 | 7 | import lombok.val; 8 | 9 | import org.apache.logging.log4j.Level; 10 | 11 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 12 | 13 | import org.minimallycorrect.tickthreading.config.Config; 14 | import org.minimallycorrect.tickthreading.log.Log; 15 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeUtil; 16 | 17 | public class LeakDetector { 18 | private static final long waitTimeSeconds = 60; 19 | private static final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("tt-clean-%d").build()); 20 | private static final Map scheduledObjects = new ConcurrentHashMap<>(); 21 | 22 | private static void scheduleCleanupTask(Object toClean, long seconds) { 23 | scheduledThreadPoolExecutor.schedule(() -> UnsafeUtil.clean(toClean), seconds, TimeUnit.SECONDS); 24 | } 25 | 26 | public static synchronized void scheduleLeakCheck(Object o, String name) { 27 | try { 28 | val clean = Config.$.worldCleaning; 29 | if (clean) 30 | scheduleCleanupTask(o, Math.min(waitTimeSeconds / 2, 20)); 31 | 32 | long id = System.identityHashCode(o); 33 | scheduledObjects.put(id, new LeakCheckEntry(o, getDescription(o, name, id), clean ? Level.TRACE : Level.WARN)); 34 | 35 | scheduledThreadPoolExecutor.schedule(scheduledObjects.remove(id)::check, waitTimeSeconds, TimeUnit.SECONDS); 36 | } catch (Throwable t) { 37 | Log.error("Failed to schedule leak check for " + name, t); 38 | } 39 | } 40 | 41 | private static String getDescription(Object o, String oDescription_, long id) { 42 | return (oDescription_ == null ? "" : oDescription_ + " : ") + o.getClass() + '@' + System.identityHashCode(o) + ':' + id; 43 | } 44 | 45 | private static class LeakCheckEntry { 46 | final WeakReference o; 47 | final String description; 48 | final Level level; 49 | 50 | LeakCheckEntry(final Object o, final String description, Level level) { 51 | this.o = new WeakReference<>(o); 52 | this.description = description; 53 | this.level = level; 54 | } 55 | 56 | void check() { 57 | if (o.get() == null) { 58 | Log.trace("Object " + description + " has been removed normally."); 59 | return; 60 | } 61 | 62 | String warning = "Probable memory leak detected. \"" + description + "\" has not been garbage collected after " + waitTimeSeconds + "s."; 63 | Log.log(level, null, warning); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/EnvironmentInfo.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util; 2 | 3 | import java.lang.management.*; 4 | 5 | import com.sun.management.UnixOperatingSystemMXBean; 6 | 7 | import org.minimallycorrect.tickthreading.log.Log; 8 | 9 | public class EnvironmentInfo { 10 | public static String getJavaVersion() { 11 | return ManagementFactory.getRuntimeMXBean().getSpecVersion(); 12 | } 13 | 14 | public static String getOpenFileHandles() { 15 | OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean(); 16 | if (osMxBean instanceof UnixOperatingSystemMXBean) { 17 | UnixOperatingSystemMXBean unixOsMxBean = (UnixOperatingSystemMXBean) osMxBean; 18 | return unixOsMxBean.getOpenFileDescriptorCount() + " / " + unixOsMxBean.getMaxFileDescriptorCount(); 19 | } 20 | return "unknown"; 21 | } 22 | 23 | public static void checkOpenFileHandles() { 24 | OperatingSystemMXBean osMxBean = ManagementFactory.getOperatingSystemMXBean(); 25 | if (osMxBean instanceof UnixOperatingSystemMXBean) { 26 | UnixOperatingSystemMXBean unixOsMxBean = (UnixOperatingSystemMXBean) osMxBean; 27 | long used = unixOsMxBean.getOpenFileDescriptorCount(); 28 | long available = unixOsMxBean.getMaxFileDescriptorCount(); 29 | if (used != 0 && available != 0 && (used > (available * 17) / 20 || (available - used) < 3)) { 30 | Log.error("Used >= 85% of available file handles: " + getOpenFileHandles()); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/PropertyUtil.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class PropertyUtil { 7 | private static final String PROPERTY_PREFIX = "tt."; 8 | 9 | public static boolean get(String name, boolean default_) { 10 | return Boolean.parseBoolean(System.getProperty(PROPERTY_PREFIX + name, String.valueOf(default_))); 11 | } 12 | 13 | public static int get(String name, int default_) { 14 | return Integer.parseInt(System.getProperty(PROPERTY_PREFIX + name, String.valueOf(default_))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/ReflectUtil.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util; 2 | 3 | import java.lang.reflect.*; 4 | 5 | public enum ReflectUtil { 6 | ; 7 | 8 | public static Field getField(Class c, String name) { 9 | Field field = null; 10 | do { 11 | try { 12 | field = c.getDeclaredField(name); 13 | } catch (NoSuchFieldException ignored) {} 14 | } while (field == null && (c = c.getSuperclass()) != Object.class); 15 | if (field != null) { 16 | field.setAccessible(true); 17 | } 18 | return field; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/Version.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | @UtilityClass 6 | public class Version { 7 | public static final String NAME = "@MOD_NAME@"; 8 | public static final boolean EXTENDED = isExtended(); 9 | public static final String RELEASE = getRelease(); 10 | public static final String DESCRIPTION = "TickThreading v@MOD_VERSION@ for MC@MC_VERSION@"; 11 | public static final String VERSION = "@MOD_VERSION@"; 12 | 13 | private static String getRelease() { 14 | return EXTENDED ? "extended" : "core"; 15 | } 16 | 17 | private static boolean isExtended() { 18 | try { 19 | Class.forName("org.minimallycorrect.tickthreading.mixin.extended.package-info"); 20 | } catch (ClassNotFoundException e) { 21 | return false; 22 | } 23 | 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/unsafe/UnsafeAccess.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util.unsafe; 2 | 3 | import java.lang.reflect.*; 4 | 5 | import sun.misc.Unsafe; 6 | 7 | import org.minimallycorrect.tickthreading.log.Log; 8 | 9 | public class UnsafeAccess { 10 | public static final Unsafe $; 11 | 12 | static { 13 | Unsafe temp = null; 14 | try { 15 | Field theUnsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe"); 16 | theUnsafe.setAccessible(true); 17 | temp = (Unsafe) theUnsafe.get(null); 18 | } catch (Exception e) { 19 | Log.error("Failed to get unsafe", e); 20 | } 21 | $ = temp; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/minimallycorrect/tickthreading/util/unsafe/UnsafeUtil.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.util.unsafe; 2 | 3 | import java.lang.reflect.*; 4 | 5 | import lombok.SneakyThrows; 6 | 7 | import sun.misc.Unsafe; 8 | 9 | import org.minimallycorrect.tickthreading.log.Log; 10 | 11 | @SuppressWarnings({"unchecked", "rawtypes"}) 12 | public class UnsafeUtil { 13 | private static final Unsafe $ = UnsafeAccess.$; 14 | private static final long baseOffset = $.arrayBaseOffset(Object[].class); 15 | private static final long headerSize = baseOffset - 8; 16 | private static final int ADDRESS_SIZE = $.addressSize(); 17 | private static final int ADDRESS_SIZE_IN_MEMORY = getAddressSizeInMemory(); 18 | private static final int MIN_SIZE = 16; 19 | 20 | @SneakyThrows 21 | private static int getAddressSizeInMemory() { 22 | Field out = System.class.getDeclaredField("out"); 23 | Field err = System.class.getDeclaredField("err"); 24 | return (int) ($.staticFieldOffset(err) - $.staticFieldOffset(out)); 25 | } 26 | 27 | public static long sizeOf(Class clazz) { 28 | if (clazz.equals(byte.class) || clazz.equals(char.class)) { 29 | return 1L; 30 | } else if (clazz.equals(short.class)) { 31 | return 2L; 32 | } else if (clazz.equals(int.class) || clazz.equals(float.class)) { 33 | return 4L; 34 | } else if (clazz.equals(long.class) || clazz.equals(double.class)) { 35 | return 8L; 36 | } else { 37 | Field[] fields = clazz.getDeclaredFields(); 38 | long sz = 0; 39 | for (Field f : fields) { 40 | if (f.getType().isPrimitive()) { 41 | sz += sizeOf(f.getType()); 42 | } else { 43 | sz += 4; //ptr 44 | } 45 | } 46 | sz += headerSize; 47 | return sz; 48 | } 49 | } 50 | 51 | /** 52 | * Creates an instance of class c without calling any constructors - all fields will be null/default primitive values, INCLUDING FINAL FIELDS. This breaks assumptions about final fields which may be made elsewhere. 53 | * 54 | * @param c Class to instantiate 55 | * @return the instance of c 56 | */ 57 | @SneakyThrows 58 | public static T createUninitialisedObject(Class c) { 59 | return (T) $.allocateInstance(c); 60 | } 61 | 62 | public static String dump(Object a) { 63 | Class c = a.getClass(); 64 | StringBuilder out = new StringBuilder(); 65 | boolean secondOrLater = false; 66 | for (Field f : c.getDeclaredFields()) { 67 | if ((f.getModifiers() & Modifier.STATIC) == Modifier.STATIC) { 68 | continue; 69 | } 70 | f.setAccessible(true); 71 | try { 72 | if (secondOrLater) { 73 | out.append('\n'); 74 | } 75 | out 76 | .append(f.getName()) 77 | .append(": ") 78 | .append(f.getType().getName()) 79 | .append("; "); 80 | Object value = f.get(a); 81 | Class vC = value.getClass(); 82 | if (vC.isArray()) { 83 | if (char[].class.equals(vC)) { 84 | int s = Array.getLength(value); 85 | for (int i = 0; i < s; i++) { 86 | out.append(Array.getChar(value, i)); 87 | } 88 | } else { 89 | int s = Array.getLength(value); 90 | for (int i = 0; i < s; i++) { 91 | out.append(Array.get(value, i)).append(','); 92 | } 93 | } 94 | } else { 95 | out.append(value); 96 | } 97 | } catch (IllegalAccessException e) { 98 | Log.error("", e); 99 | } 100 | secondOrLater = true; 101 | } 102 | return out.toString(); 103 | } 104 | 105 | /** 106 | * Sets all non-primitive/array fields of o to null. For use when you know some stupid mod/plugin is going to leak this object, and want to leak only the size of the object, not everything it references. 107 | * 108 | * @param o Object to clean. 109 | */ 110 | public static void clean(Object o) { 111 | //Log.info("Clearing " + o.getClass() + '@' + System.identityHashCode(o)); 112 | Class c = o.getClass(); 113 | while (c != null) { 114 | for (Field field : c.getDeclaredFields()) { 115 | if ((!field.getType().isArray() && field.getType().isPrimitive()) || (field.getModifiers() & Modifier.STATIC) == Modifier.STATIC) { 116 | continue; 117 | } 118 | try { 119 | field.setAccessible(true); 120 | field.set(o, null); 121 | //Log.info("Cleaned field " + field.getType().getName() + ':' + field.getName()); 122 | } catch (IllegalAccessException e) { 123 | Log.warn("Exception cleaning " + o.getClass() + '@' + System.identityHashCode(o), e); 124 | } 125 | } 126 | c = c.getSuperclass(); 127 | } 128 | } 129 | 130 | @SuppressWarnings("deprecation") 131 | public static void stopThread(Thread t, Throwable thr) { 132 | try { 133 | t.stop(thr); 134 | } catch (Throwable ignored) { 135 | try { 136 | Method m = Thread.class.getDeclaredMethod("stop0", Object.class); 137 | m.setAccessible(true); 138 | m.invoke(t, thr); 139 | } catch (Throwable throwable) { 140 | Log.error("Failed to stop thread t with throwable, reverting to normal stop.", throwable); 141 | t.stop(); 142 | } 143 | } 144 | } 145 | 146 | @SneakyThrows 147 | public static void removeSecurityManager() { 148 | if (System.getSecurityManager() == null) 149 | return; 150 | 151 | Field err = System.class.getDeclaredField("err"); 152 | $.putObjectVolatile($.staticFieldBase(err), $.staticFieldOffset(err) + ADDRESS_SIZE_IN_MEMORY, null); 153 | 154 | if (System.getSecurityManager() != null) 155 | Log.error("Failed to remove SecurityManager"); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/main/resources/mcmod.info: -------------------------------------------------------------------------------- 1 | [{ 2 | "modid": "${modid}", 3 | "name": "${name}", 4 | "description": "Mod which runs entity and tileEntity ticks in threads.", 5 | "version": "${version}", 6 | "mcversion": "${mcversion}", 7 | "url": "https://github.com/MinimallyCorrect/TickThreading", 8 | "authorList": [ "nallar" ], 9 | "credits": "Authored by nallar" 10 | }] 11 | -------------------------------------------------------------------------------- /src/main/resources/patches/minecraft-extended.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | run 7 | 8 | 9 | 12 | 13 | startSection,endStartSection,endSection 14 | 15 | 16 | setWorld 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/patches/minecraft.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/test/java/org/minimallycorrect/tickthreading/unsafe/UnsafeUtilTest.java: -------------------------------------------------------------------------------- 1 | package org.minimallycorrect.tickthreading.unsafe; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertNull; 5 | 6 | import java.security.*; 7 | 8 | import org.junit.Test; 9 | 10 | import org.minimallycorrect.tickthreading.util.unsafe.UnsafeUtil; 11 | 12 | public class UnsafeUtilTest { 13 | private static void installAnnoyingSecurityManager() { 14 | System.setSecurityManager(new SecurityManager() { 15 | @Override 16 | public void checkPermission(Permission perm) { 17 | String permName = perm.getName() != null ? perm.getName() : "missing"; 18 | if (permName.startsWith("exitVM") || "setSecurityManager".equals(permName)) 19 | throw new SecurityException("Cannot " + permName); 20 | } 21 | }); 22 | } 23 | 24 | @Test 25 | public void testRemoveSecurityManager() throws Exception { 26 | installAnnoyingSecurityManager(); 27 | 28 | assertNotNull(System.getSecurityManager()); 29 | 30 | UnsafeUtil.removeSecurityManager(); 31 | 32 | assertNull(System.getSecurityManager()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | #Version of the produced binaries. This file is intended to be checked-in. 2 | #It will be automatically bumped by release automation. 3 | version=0.0.1 4 | previousVersion=0.0.0 5 | --------------------------------------------------------------------------------