Speed up imports and add a CI

What does this PR do?
This PR introduces a CI which utilizes the tuna and my tuna-interpreter libraries to perform time-based import tests.
It was reported by @stas00 that we were taking far too long to do basic things like accelerate launch, and tuna can help visualize why by creating import graphs directing us to what is taking too long:

I wrote a small library called tuna-interpreter that aims to take the best parts of tuna and work it into something parse-able that lets us run CIs off of it.
After using the tool:

We can see a decrease of ~68%
How it works:
In its current form, we are going based off of a baseline torch import, since Accelerate relies on torch no matter what. BUT we should be no more than ~20% slower than the torch import overall. Anything more and we have some slip-up or timing problem.
An example test looks like the following:
    def test_base_import(self):
        output = run_import_time("import accelerate")
        with open(f"{self.tmpdir}/base_results.log", "w") as f:
            f.write(output)
        data = read_import_profile(f"{self.tmpdir}/base_results.log")
        total_time = calculate_total_time(data)
        pct_more = total_time / self.pytorch_time
        # Base import should never be more than 10% slower than raw torch import
        err_msg = f"Base import is more than 20% slower than raw torch import ({pct_more * 100:.2f}%), please check the attached `tuna` profile:\n"
        sorted_data = sort_nodes_by_total_time(data)
        paths_above_threshold = get_paths_above_threshold(sorted_data, 0.1, max_depth=7)
        err_msg += f"\n{convert_list_to_string(paths_above_threshold)}"
        self.assertLess(pct_more, 1.2, err_msg)

Where essentially we:

Get the import_time to run a particular python import called via -c in subprocess
We then read this profile generated
From here, we take all the nodes, sort them by time, and get all paths above an arbitrary threshold. This should be tweaked to your own discretion, as threshold and max_depth changes from library to library. The key with max_depth is it should be enough to get your imports out of the slowdown trace, and show what external libraries you are really calling.
Afterwards, we write a note stating that it was above a slowdown expected %, and state what modules were slowing it down.

An example failure is below, where we can clearly see what module chain had the slowdown:
E       AssertionError: 1.3515017627366224 not less than 1.2 : Base import is more than 20% slower than raw torch import (135.15%), please check the attached `tuna` profile:
E
E       main 0.973s
E       main->accelerate 0.961s
E       main->accelerate->accelerate.accelerator 0.959s
E       main->accelerate->accelerate.accelerator->torch 0.758s
E       main->accelerate->accelerate.accelerator->torch->torch._C 0.355s
E       main->accelerate->accelerate.accelerator->torch->torch._meta_registrations 0.154s
E       main->accelerate->accelerate.accelerator->torch->torch._meta_registrations->torch._decomp 0.109s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing 0.126s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils 0.125s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils->accelerate.utils.megatron_lm 0.107s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils->accelerate.utils.megatron_lm->transformers.modeling_outputs 0.106s

tests/test_imports.py:64: AssertionError

Or:
E       AssertionError: 1.8292819779293377 not less than 1.2 : Base import is more than 20% slower than raw torch import (182.93%), please check the attached `tuna` profile:
E
E       main 1.324s
E       main->accelerate 1.308s
E       main->accelerate->accelerate.accelerator 1.307s
E       main->accelerate->accelerate.accelerator->torch 0.706s
E       main->accelerate->accelerate.accelerator->torch->torch._C 0.327s
E       main->accelerate->accelerate.accelerator->torch->torch._meta_registrations 0.152s
E       main->accelerate->accelerate.accelerator->torch->torch._meta_registrations->torch._decomp 0.108s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing 0.527s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils 0.526s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils->accelerate.utils.fsdp_utils 0.488s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils->accelerate.utils.fsdp_utils->torch.distributed.fsdp.fully_sharded_data_parallel 0.488s
E       main->accelerate->accelerate.accelerator->accelerate.checkpointing->accelerate.utils->accelerate.utils.fsdp_utils->torch.distributed.fsdp.fully_sharded_data_parallel->torch.distributed.fsdp 0.488s

tests/test_imports.py:64: AssertionError

If there are specific issues with using tuna-interpreter, please let me know, it's a very quickly hacked-together-but-working library for what we are doing, and open to improving it further after we battle-test it
Before submitting

[ ] This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
[x] Did you read the contributor guideline,
Pull Request section?
[ ] Was this discussed/approved via a Github issue or the forum? Please add a link
to it if that's the case.
[ ] Did you make sure to update the documentation with your changes? Here are the
documentation guidelines, and
here are tips on formatting docstrings.
[x] Did you write any new necessary tests?

Who can review?
Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.
@SunMarc @BenjaminBossan @sayakpaul @Titus-von-Koeller @ArthurZucker @ydshieh @LysandreJik

Let me know how I can improve on this tool further so we can then get it going throughout anyone at HF that wants to use it 🤗

@BenjaminBossan what's missing here is the initial tuna check, which is why it's required (see the workflow).
We can eventually look at gutting it out, sure. However I think having both the visual option for debugging further + the condensed output here is valuable and is why we should still use tuna itself.

@BenjaminBossan fully removed the requirement for tuna there, however I still think it's useful for further debugging so left that in as part of the testcase class description :)

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

🔥


Accordion

Accordions are versatile and help maintain a clean, organized user interface, particularly when dealing with content that can be logically grouped or when you want to present information in a hierarchical manner, making it easier for users to access what they need without overwhelming them.
We can use it to answer some frequently asked questions

please assign this to me

Hi @rajan98 thanks for raising this issue ❤ You can start working on this I have assigned you this issue 😄

Hi @rajan98 thanks for raising this issue ❤ You can start working on this I have assigned you this issue 😄
I have made the Accordion and made a pull request please review and merge it.


Cf saas oauth

[CI] Staging environment is at http://cap-staging-docs.codefresh.io.s3-website.us-east-1.amazonaws.com/cf-saas-oauth/docs/


The Translation for „Hygienic Wash“ in DE is wrong

In the german translation the „Hygienic wash“ program is incorrectly translated into „Empfindliches“ which is quite the opposite.
The corresponding translation for „Hygienic Wash“ would be „Hygienewäsche“ in german.
I started the program on the machine and HC-integration got the wrong translation for the program code.

-Latest version of integration

This translation is only used for LaundryCare.Washer.Program.Sensitive, not for hygienic wash.

The washing machine is definetly set to "Hygienic wash" wenn "empfindliches" is shown in the Home Assistant UI.
For the dryer of the same generation that I also own the translation is shown correct.
The only other reason that I can think of is that the washer is giving the wrong status. This would be an issue with the appliances firmware itself.

Please set the machine to the hygienic wash mode, verify that you see the wrong translation and then press the Home Connect Debug Info button in the HA dashboard. Then upload the log file here.
You can also try switching to Server Translation mode and check what translation you see there.


Rewrite Semaphore.acquire/release

For #270
Add a failing test for original Semaphore: when a ready waiter be cancelled, the other pending waiters will not aware of it.

test_acquire in tests/test_locks.py assumes that if task A calls semaphore.acquire() before task B, A will acquire the semaphore before B. I use self._num_ready to make sure that if there is any awoken waiter, the new acquire will not cut in line.
test_acquire_cancel in tests/test_locks.py doesn't allow Semaphore to leaving cancelled waiters in Semaphore._waiters.

I'm not sure whether or not these two tests should be modified.

Following the discussion of #269, the behavior of asyncio.Semaphore now obeys the semantic of the semaphore.

There is no guarantee that task A will acquire the semaphore before task B if A called semaphore.acquire() before B.

Now the implementation of Semaphore is the same as Queue's.
I am still thinking over fut.cancel() in Semaphore.acquire. Following the discussion of #269, I understand that the Future might be not done under some scenarios. According to the documentation of asyncio: (https://docs.python.org/3/library/asyncio-task.html)

result = await future or result = yield from future – suspends the coroutine until the future is done ...

Will assert fut.done() be another choice which could let the user notice that there might be something wrong?

I need a little time to review this. I think you're on the right track though!

I found that my comment above is wrong.

the behavior of asyncio.Semaphore now obeys the semantic of the semaphore.

asyncio.Semaphore is not exactly the same as normal semaphore. The sequence of the tasks acquiring the asyncio.Semaphore is deterministic under some scenarios.
For example, suppose there are only two tasks A and B. A is waiting for the semaphore sem. If we call sem.acquire() following sem.release() in task B, B will acquire sem before A. I'm not sure whether or not "changing the container of pending Futures to a list of randomly inserted elements" is superfluous.
Anyway, I should not say "asyncio.Semaphore now obeys the semantic of the semaphore."

Thanks! I've added some nits, let me know when you've addressed them and I'll merge this baby.

Oh, one more thing. You need to have an account on bugs.python.org and you need to sign the online PSF contributor form: https://www.python.org/psf/contrib/contrib-form/ I will then check your status in the bug tracker so we know you've signed it. (This only needs to happen for your first contribution to Python. :-)

@gvanrossum
I changed the condition of the while loop in acquire and restored the blank lines. And I created an account on bugs.python.org and signed the form.
Thank you very much for the review!

Oh and I think that when a pending task being deleted (raise GeneratorExit in Semaphore.acquire, Queue.put or Queue.get) there might be some bugs though ...

The generatorExit will be raised when close of the genenator underlying the wrapping task is called. This may happen when the semaphore and the event loop (and all the other things reference to the task) are destroyed, and the pending task is going to be deleted. Maybe we don't need to worry about whether or not the state of the destroyed semaphore is correct.
On the other hand, the document says

result = await future or result = yield from future – suspends the coroutine until the future is done ...

A pending task will be awoken by a callback function _wakeup, which will call Task._step of the task. And _wakeup will be scheduled if and only if the corresponding future is done. I am not sure whether or not "the spirit of yield from future implies that when we pass this line, we know the future is done".

We certainly can trust that when we pass yield from future without raising an exception the future is done. What we can't trust is whether it will be done when we catch an exeption, especially a BaseException instance (which is what GeneratorExit is). The philosophical question is then whether we might still care about the semaphore or queue when the generator is closed. I think it's at least conceivable that some code could create the generator, let it the event loop run for a little bit, and then decide to close the generator. So preserving the invariant of the queue or semaphore in this case sounds good. And that's what we do. So I'm going to merge your change!

Applied cc20f3951e81b8bbd405d9100c17a0b94c22b0b9.
I did a manual merge, so you may have to mark the PR as applied manually. Thanks!!

I apologize for sticking to this issue for several days. I want to make sure whether or not "the exceptions other than CancelledException should be take into account in other places using await future (or yield from future)."

... some code could create the generator, let it the event loop run for a little bit, and then decide to close the generator ...

I think that a generator G (or a coroutine) calling await future must be wrapped in a Task T scheduled by the event loop. To close the generator, maybe the programmer should call T.cancel() instead of calling close of underlying G (if the programmer can reference G). I can't figure out under what scenario a user of asyncio is able to and would like to operate the wrapped G directly.
When I was testing Queue, I got GeneratorExit when the program was going to exit and there are still  some pending task which I didn't cancel before the program finished. I didn't know how to call close of the underlying generator without using Task._coro. As a user of asyncio, I think I should not use _coro with a leading underscore.

mark the PR as applied manually

Do I just leave a comment on this PR, or there are other steps I miss?

Here's a way that GeneratorExit can be caused:
from asyncio import coroutine, sleep, ensure_future, get_event_loop

@coroutine
def foo():
    try:
        yield from sleep(1)
    except BaseException as err:
        print('hoho')

@coroutine
def bar():
    coro = foo()
    task = ensure_future(coro)
    yield from sleep(0.1)
    coro.close()

get_event_loop().run_until_complete(bar())

However I don't think that this is a very realistic scenario. Another scenario is when you receive a KeyboardInterrupt. Since that is a BaseException, it barrels right past except Exception: clauses elsewhere in asyncio.
Looking back at it I think that's probably a mistake -- when we use try/finally, the finally action definition happens even when a BaseException is raised, but more explicit cleanup in except Exception: blocks ignores it. It would be nice if all fundamental internal data structures remained in a consistent state even when BaseException (of whatever nature -- GeneratorExit, SystemExit or KeyboardInterrupt) left things in a consistent state, just in case some later recovery process fires up the event loop again.
PS. I think just closing the issue was enough to take the PR off the list.

The thought that "calling coro.close() instead of task.cancel() in bar is a wrong use of asyncio" is what makes me stick to GeneratorExit. I think I'm too naive to imagine these things. Thank you very much for clarify all of these issues!!!

It is wrong to do that. But if we can write the code robustly, so that we don't care about wrong things people do, without making it unreadable or obfuscated or slower, we should write the code robustly. And I think that's what we've accomplished. Do you agree?

Yes, I do. I learned a lot from the discussion! Thank you for all your help and guidance!


Add Mocked Tests: Image Actions

This PR is adding mocked tests for image_actions.get, image_actions.list, image_actions.post
Related to https://github.com/digitalocean/pydo/issues/211
Notes:

We may want to fix Makefile:  PYLINT_ARGS is empty for make lint-tests.
Not sure what the intention is: should we remove PYLINT_ARGS or assign the same value as in github actions - https://github.com/digitalocean/pydo/blob/main/.github/workflows/pr.yaml#L40

.PHONY: lint-tests
lint-tests: install ## Lints the code
	poetry run black --check --diff tests/. && \
	poetry run pylint $(PYLINT_ARGS) tests/.

Please advise on TODOs in file

We may want to fix Makefile: PYLINT_ARGS is empty for make lint-tests.
Not sure what the intention is: should we remove PYLINT_ARGS or assign the same value as in github actions -  https://github.com/digitalocean/pydo/blob/main/.github/workflows/pr.yaml#L40

We set PYLINT_ARGS="--disable=fixme" in the GitHub workflow so that TODO comments do not blocker pull requests as we have a number of them in the code base. When run locally, it may be useful to quickly find where we have TODO comments that still need to be addressed. We pass the empty PYLINT_ARGS in the Makefile so that it is passed through in the environment. If you want to disable the warnings locally, you can run:
PYLINT_ARGS="--disable=fixme" make lint-tests