Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Core: Lazy loading experiment #4573

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

Mysteryem
Copy link
Contributor

@Mysteryem Mysteryem commented Jan 29, 2025

What is this fixing or adding?

Implements lazy loading by reading the game for each world from an ap_info.json (similar to the archipelago.json suggested in #4516), intercepting worlds.<world> module loading, and allowing worlds to be loaded by game name.

There is currently no intention to merge this PR, it has been created to aid with discussion surrounding the problem of lazy loading. I think there is more planning and design discussion required before lazy loading can be properly implemented into Archipelago.

  • worlds.ensure_worlds_loaded(): Load worlds by game name or load all worlds
  • worlds.ZipWorldLoader, worlds.SourceWorldLoader and worlds.BytecodeWorldLoader implement the module loading
    • The latter two are not strictly required because Python can load them fine on its own, but using them provides easier module execution benchmarking
  • worlds.WorldFinder is an importlib.abc.MetaPathFinder, it is responsible for handling attempts to import worlds.<world> modules that have not been loaded yet, and picking the correct Loader for the world.
    • The WorldFinder functions by being prepended to sys.meta_path: sys.meta_path.insert(0, WorldFinder()). This is necessary to intercept loading of non-zipped worlds when running from source, but it can be appended to sys.meta_path instead to only handle loading of zipped worlds, which might be better for frozen builds (assuming things actually work on frozen builds)

For how module importing/loading works in Python, I would recommend reading through most of https://docs.python.org/3/library/importlib.html. I did not know much about module importing/loading before I started working on this PR and learned pretty much everything necessary to implement this PR from that documentation.

Known broken things

  • meta.yaml parsing is broken
    • This happens before game yamls are parsed to determine what worlds need to be loaded. I have not looked into how to fix this, nor do I know how broken it is.
  • Worlds must not be loaded while generation is ongoing on the same process
    • Loading a world can add to CollectionState.additional_copy_functions and CollectionState.additional_init_functions, so loading a world while generation is ongoing can add a copy function and then attempting to copy multiworld.state will fail because the attributes the copy function was expecting to be created by the init function do not exist.
    • Multiple generations on WebHost occurring on the same process is assumed to be broken for this reason
    • Generation (Main.main) and worlds.ensure_worlds_loaded() most likely need to use locks to prevent concurrent generations on the same process, as well as anything else on the same process that might try to load worlds. Care will be needed with implementing this to ensure that deadlocks cannot happen.

General information

  • The games specified in player yamls are what load the required worlds in generation. If an unknown game is found, all worlds that did not provide an ap_info.json are attempted to be loaded to try to find a matching world.
  • Launcher.py has to load every world so that the clients in the launcher get populated
  • Opening clients from the launcher works because importing a world module at any point will load that module
  • Opening a patch file with the launcher attempts to read the game from the archipelago.json within the patch for patches that use Patch.create_rom_file(), and then load the world for that game
  • Patches that use APPatchExtension/AutoPatchExtensionRegister already provide their game, so it is easy to then ensure the world for that game is loaded
  • ALTTP always gets loaded due to core Archipelago still being intertwined with it
  • I don't understand a lot of the WebHost code and how it connects together with what is run on separate threads/processes. Much of WebHost has been slapped with a "stick worlds.ensure_all_worlds_loaded() at the start and pray"
  • Lots of tests had to be modified to load all worlds. It's possible some tests have been missed.

Potential improvements

  • Actually scanning through the world folders (worlds.scan_for_worlds_on_module_load()) should be deferred until the first attempt to load a world
  • If a world does not provide an ap_info.json it is probably possible to determine the package of a loaded world class and create the ap_info.json for that world

How was this tested?

Unit tests have been run (note that if I missed updating some tests that iterate through all worlds, those tests would now be iterating no worlds, so would probably succeed and test nothing).

Seeds have been generated locally.

Solo seeds have been generated on webhost and a solo seed has been played a small amount while connected to a webhost hosted room. This solo seed also used a game with a patch file, so that was tested on the client side too.

worlds.BytecodeWorldLoader was tested by replacing the Clique source files with a copy of the Clique world from a frozen build of AP 0.5.1.

Frozen builds have not been tested at all.

No more hacks for setting module/package of worlds loaded from zip
files. Though unsure how this is working.

Now handles loading of non-zip worlds too.

World load times now include only the duration of exec_module in the
module loader, which should be fairer on modules that have to be loaded
from a .zip.
Look for the archipelago.json in the patch file zip and load the
game specified in the archipelago.json.

I don't know if APPatchExtension (AutoPatchExtensionRegister) without a
game specified will work.
There could still be some missing or some of these might not be
necessary.
The Temp World was copying the ap_info.json used by clique, so was
thinking it was also Clique.
Trying to load all worlds here would load all worlds even when trying to
generate with only a few worlds.

The late loading of these worlds would result in worlds adding copy/init
mixins after CollectionState instances have already been created,
meaning that copying any existing CollectionState would fail by trying
to copy attributes the new copy mixins were expecting to exist, but did
not because the CollectionState was created before the init mixin was
added.

This probably means it would be a good idea for there to be a function
that can be called to prevent further world loading. Possibly raising an
error if such loading is attempted.
Only load the worlds for the games present in the embedded data package.
The "Archipelago" world (worlds.generic) always needs to be loaded so
that its items and locations can be added to each data package.
WebHostContext, unlike Context, needed to call self._load_game_data()
immediately. This has been moved to its __init__() right after the
super().__init__() and its _load_game_data now does nothing.

get_static_server_data() now ensures all the worlds get loaded before
caching the data. Not sure how I missed this usage.
In the original code, the ZipImporter is told to find the spec, but it
does not know that "<world name>" in the zip should really be providing
"world.<world name>", so the spec it creates is for importing a
"<world name>" module, so after importing AP had to mess with the module
and package name to correct it. It's probably possible to fix this
separately on the main branch.
The set could be constructed programmatically based on knowing which
games need to be loaded and subtracting games that have already been
loaded, resulting in an empty set when all required games have already
been loaded. In this case, no worlds would need to be loaded.
Removes warnings about shadowing global variables.
Loading worlds late into generation can result in confusing errors from
CollectionState copy_mixins failing due to expected init_mixins not
being run because the CollectionState was created before the world was
loaded.
typing.override was only added in Python 3.12
@github-actions github-actions bot added affects: core Issues/PRs that touch core and may need additional validation. affects: webhost Issues/PRs that touch webhost and may need additional validation. labels Jan 29, 2025
# Prevent additional world loading as a safeguard.
# Additional worlds must not be loaded after a CollectionState has been created because worlds may add init_mixins
# and copy_mixins to the CollectionState class.
worlds.world_loading_enabled = False
Copy link
Contributor Author

@Mysteryem Mysteryem Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the PR description, for WebHost, Main.main (generation) and worlds.ensure_worlds_loaded probably need to use a lock to properly prevent world loading while a generation is ongoing on the same process.

worlds.world_loading_enabled is also currently not set back to True anywhere.

This was not reverted correctly and ended up above LogicMixin when it
should have been below LogicMixin
@Mysteryem Mysteryem changed the title Lazy loading experiment Core: Lazy loading experiment Jan 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affects: core Issues/PRs that touch core and may need additional validation. affects: webhost Issues/PRs that touch webhost and may need additional validation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant