Uploaded image for project: 'Minecraft: Java Edition'
  1. Minecraft: Java Edition
  2. MC-216347

Game freezes when placing player heads with custom NBT data

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Duplicate
    • None
    • 1.16.5, 21w07a
    • None
    • Unconfirmed
    • (Unassigned)

      The game freezes for up to two seconds (twice in sequence) every time I place a custom player head with a missing id parameter

      To reproduce give yourself a custom head using these NBT tags:

      { "SkullOwner": {"Name": "Notarealplayersnameplsfixthismojang",    "Properties": {   "textures": [{ "Value": "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmQzNGIzZTI3YTNmZTUzODI3YjM3YWQ1OTU2YWNjYTA4ZjI4NjNjNjkyNmNjOTcxMTZkZGEzMzQ4Njk3YTVhOSJ9fX0" }]}}}
      

       

      Edit: One slight amendment: The "Properties" attribute here may not be required to reproduce the error, as in addition to injecting a random UUID I remember finding that the game would strip all the properties as well.

       

      From my own investigations, this seems to be multiple things going wrong at once:

      1. Notice that the NBT specifies a game profile with no "ID". By my own debugging I've found that the game profile that ends up being used to render the block has been given a random UUID, probably inserted by something else though I couldn't figure out where.
      2.  Because the profile has a junk UUID, the game ends up trying to "fix" the profile as per the following code in SkullBlockEntity (edited for clarity):
          @Nullable
          public static GameProfile loadProperties(@Nullable GameProfile gameProfile) {
              if (gameProfile != null && !ChatUtil.isEmpty(gameProfile.getName())) {
                  if (gameProfile.isComplete() && gameProfile.getProperties().containsKey("textures")) {
                      return gameProfile;
                  } else if (userCache != null && sessionService != null) {
                      GameProfile gameProfile2 = userCache.findByName(gameProfile.getName());
                      if (gameProfile2 != null) {
                          Property property = Iterables.getFirst(gameProfile2.getProperties().get("textures"), null);
                          if (property == null) {
                              gameProfile2 = sessionService.fillProfileProperties(gameProfile2, true);
                          }
                      }
                      return gameProfile2;
                  }
              }
              
              return gameProfile;
          }
      
      1. This goes into UserCache#findByName which calls UserCache#findProfileByName which itself calls GameProfileRepository#findProfilesByNames located in YggdrasilGameProfileRepository.
      2. Finally, YggdrasilGameProfileRepository does a number of this:
        1. It calls
          authenticationService.makeRequest(...);

          Which takes a few seconds, then

        1. It calls
          Thread.sleep(DELAY_BETWEEN_PAGES);

          Which stalls the thread for 100ms

        1. If/When it fails, it then calls
      Thread.sleep(DELAY_BETWEEN_FAILURES);

      Which stalls the thread for an additional 750ms

        1. Finally, it repeats the above up to 3 times if it fails

      Based on the above analysis, I can conclude that when placing a head it does the first page request and then unconditionally waits the 100ms, resulting in the pattern of two freezes (one long, one short) in quick succession as observed in the beginning.

       

      My suggestion, since it takes a result callback and looks like it was intended to be called in a worker thread, is that you should really punt this over to a worker thread.

      That can be easily done using a LoadingCache:

      private static UserCache userCache;
      @Nullable
      private static MinecraftSessionService sessionService;
      private static final LoadingCache<GameProfile, CompletableFuture<GameProfile>> profileResolution = CacheBuilder.newBuilder()
                  .expireAfterAccess(15, TimeUnit.SECONDS)
                  .build(CacheLoader.from(profile -> {
                      return CompletableFuture.supplyAsync(() -> {
                          GameProfile p = userCache.findByName(profile.getName());
                          if (p != null) {
                               Property property = Iterables.getFirst(p.getProperties().get("textures"), null);
                                if (property == null) {
                                      p = sessionService.fillProfileProperties(p, true);
                                }
                                return p;
                          }
                          return profile;
                      }, Util.getMainWorkerExecutor());
                  }));
      public GameProfile findFixedProfile(GameProfile profile) {
         return profileResolution.getUnchecked(profile).getNow(profile);
      }
      

      But also I would look into why it's giving ids to game profiles that don't exist and were not specified to have one.

       

       Edit 2: After fixing my environment I was able to look a little closer and it looks like the second culprit is the call to

      sessionService.fillProfileProperties(gameProfile2, true) 

      ...So I've made the necessary adjustments to my recommendation.

       

      Code analysis + investigation was done in a decompiled development environment based on 21w05b and the Yarn mappings. The issue was also reproduced and confirmed present in a vanilla/official build of 21w07a using the regular launcher with default settings.

       

            Unassigned Unassigned
            awr_* Sollace
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: