Lazy loading of service descriptions#11479
Conversation
| 'fields': description.get('fields', {}) | ||
| } | ||
|
|
||
| @bind_hass |
|
|
||
| return service_ent_id | ||
|
|
||
| @bind_hass |
| 'fields': description.get('fields', {}) | ||
| } | ||
|
|
||
| @bind_hass |
|
|
||
| return service_ent_id | ||
|
|
||
| @bind_hass |
MartinHjelmare
left a comment
There was a problem hiding this comment.
I like the clean up. Downside is that it's less clear that the services have descriptions.
|
|
||
| if domain == ha.DOMAIN: | ||
| import homeassistant.components as components | ||
| file = components.__file__ |
There was a problem hiding this comment.
file is a python builtin. Please use another variable name, eg fil, comp_file or similar.
|
|
||
| descriptions = DESCRIPTION_CACHE.get(file) | ||
| if descriptions is None: | ||
| descriptions = load_yaml_config_file( |
There was a problem hiding this comment.
This call will do I/O. Since get_description will be called from async_get_all_descriptions, the function that does I/O needs to be scheduled on the executor.
There was a problem hiding this comment.
Oh. This was moved from the LIFX async_setup_platform but I guess that was also wrong.
I am a bit confused by the main/executor transitions available. What is the proper way to schedule the function in this situation?
There was a problem hiding this comment.
I think the function that schedules the job for the executor, via hass.async_add_job, needs to be a coroutine to be able to yield and wait for the result, ie to have the descriptions ready. Not 💯 though. I'd wait for a core review before changing anything.
| return {domain: {key: value.as_dict() for key, value | ||
| in self._services[domain].items()} | ||
| for domain in self._services} | ||
| return {domain: self._services[domain].copy() for domain in self._services} |
53c8f61 to
aff3d73
Compare
| descriptions[domain] = {} | ||
| for service in services[domain]: | ||
| description = yield from hass.async_add_job( | ||
| get_description, hass, domain, service) |
There was a problem hiding this comment.
Since we know that we're going to load all the descriptions, it would help if we preload the catch all services file (/components/services.yaml). That file is pretty heavy if we have to parse it X times.
There was a problem hiding this comment.
It should only be read and parsed once, that's what the DESCRIPTION_CACHE is for.
There was a problem hiding this comment.
Oh I see now that you cache that file. I'm not sure if that is a great idea, it's big and a lot will be unused.
|
I love this ❤️ It doesn't make sense to keep it in core. |
|
Once removed from all components/platforms, should really speed up the startup and tests 👍 |
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| DESCRIPTION_CACHE = {} |
There was a problem hiding this comment.
Don't store the description cache in a global. Instead store it in hass.data, which is a dict for globals during the runtime of a hass instance.
|
|
||
| descriptions = DESCRIPTION_CACHE.get(comp_file) | ||
| if descriptions is None: | ||
| descriptions = load_yaml_config_file( |
| if descriptions is None: | ||
| descriptions = load_yaml_config_file( | ||
| path.join(path.dirname(comp_file), 'services.yaml')) | ||
| DESCRIPTION_CACHE[comp_file] = descriptions |
There was a problem hiding this comment.
Would it make sense to keep a cache per file or would a cache per service be better?
| """Return descriptions (i.e. user documentation) for all service calls.""" | ||
| FILE_CACHE = {} | ||
|
|
||
| if not SERVICE_DESCRIPTION_CACHE in hass.data: |
There was a problem hiding this comment.
test for membership should be 'not in'
| for domain in services: | ||
| descriptions[domain] = {} | ||
| for service in services[domain]: | ||
| descriptions[domain][service] = yield from hass.async_add_job( |
There was a problem hiding this comment.
Your code would be a lot faster if you test if the key exists in the cache inside the async context (before this line). No need to yield and wait for a thread to run our job.
|
Super excited about this PR ! It looks like we're saving around ~1 minute on a full test run! (8m23s -> 7m23s) |
|
I reworked it once again, this time with more attention to performance. So now we only Can't say I notice any difference on my dev setup, though. Maybe it will be more significant on slow machines with a cold disk cache, or maybe not. @balloob Are the timings from your local setup? I still only use Travis and it is so erratic that I cannot tell whether there is any improvement at all. |
| msg['id'], self.hass.services.async_services())) | ||
| @asyncio.coroutine | ||
| def call_service_helper(msg): | ||
| """Call a service and fire complete message.""" |
There was a problem hiding this comment.
The function name and docstring are stale. This function doesn't call a service.
|
|
||
| def format_cache_key(domain, service): | ||
| """Build a cache key.""" | ||
| return "{}.{}".format(domain, service) |
There was a problem hiding this comment.
I'd just store the format string as a constant instead of returning it inside a function. You'll save one function call like that.
|
Those were timings from Travis. I took the build times from your branch and compared it to the latest dev branch. Not very scientific as container speeds fluctuate. |
|
🎉 🐬 🌮 💃 🍻 |
|
The device tracker tests failed frequently with this change. I think I finally figured out that it was a couple of existing races that got easier to hit after the YAML loading was removed. |
|
Did a test run on my Macbook Pro Retina 2012: Before: After: That's a great improvement! 👍 |
|
(is_allowed_path always fails on Mac because |
Description:
With this PR the loading of
services.yamlis postponed until it is needed, namely when listing services inwebsocket_apiandapi.This enables significant code cleanup in platforms but my main motivation was to allow
custom_componentstesting without having to copyservices.yamlaround.Checklist:
If the code does not interact with devices:
toxrun successfully.